diff options
168 files changed, 4806 insertions, 2350 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 04ca4070d..61ee1d9ff 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -1,5 +1,5 @@ <!-- All PR should follow this template to allow a clean and transparent review --> -<!-- Text placed between these delimiters is considered a commend and is not rendered --> +<!-- Text placed between these delimiters is considered a comment and is not rendered --> ## Change Summary <!--- Provide a general summary of your changes in the Title above --> @@ -27,7 +27,10 @@ interface_definitions: $(config_xml_obj) find $(BUILD_DIR)/interface-definitions -type f -name "*.xml" | xargs -I {} $(CURDIR)/scripts/build-command-templates {} $(CURDIR)/schema/interface_definition.rng $(TMPL_DIR) || exit 1 # XXX: delete top level node.def's that now live in other packages - rm -f $(TMPL_DIR)/firewall/node.def + # IPSec VPN EAP-RADIUS does not support source-address + rm -rf $(TMPL_DIR)/vpn/ipsec/remote-access/radius/source-address + # T3568: firewall is yet not migrated to XML and Python - this is only a dummy + rm -rf $(TMPL_DIR)/firewall/node.def rm -rf $(TMPL_DIR)/nfirewall # XXX: test if there are empty node.def files - this is not allowed as these # could mask help strings or mandatory priority statements @@ -86,7 +89,7 @@ clean: .PHONY: test test: - set -e; python3 -m compileall -q . + set -e; python3 -m compileall -q -x '/vmware-tools/scripts/' . PYTHONPATH=python/ python3 -m "nose" --with-xunit src --with-coverage --cover-erase --cover-xml --cover-package src/conf_mode,src/op_mode,src/completion,src/helpers,src/validators,src/tests --verbose .PHONY: sonar diff --git a/data/configd-include.json b/data/configd-include.json index a03360bdb..3b4e2925b 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -69,6 +69,5 @@ "vpn_pptp.py", "vpn_sstp.py", "vrf.py", -"vrrp.py", -"vyos_cert.py" +"vrrp.py" ] diff --git a/data/templates/accel-ppp/sstp.config.tmpl b/data/templates/accel-ppp/sstp.config.tmpl index 7ca7b1c1e..fad91d118 100644 --- a/data/templates/accel-ppp/sstp.config.tmpl +++ b/data/templates/accel-ppp/sstp.config.tmpl @@ -29,9 +29,9 @@ disable verbose=1 ifname=sstp%d accept=ssl -ssl-ca-file={{ ssl.ca_cert_file }} -ssl-pemfile={{ ssl.cert_file }} -ssl-keyfile={{ ssl.key_file }} +ssl-ca-file=/run/accel-pppd/sstp-ca.pem +ssl-pemfile=/run/accel-pppd/sstp-cert.pem +ssl-keyfile=/run/accel-pppd/sstp-cert.key {# Common IP pool definitions #} {% include 'accel-ppp/config_ip_pool.j2' %} diff --git a/data/templates/ethernet/wpa_supplicant.conf.tmpl b/data/templates/ethernet/wpa_supplicant.conf.tmpl index fe518ad45..308d777f1 100644 --- a/data/templates/ethernet/wpa_supplicant.conf.tmpl +++ b/data/templates/ethernet/wpa_supplicant.conf.tmpl @@ -32,11 +32,11 @@ fast_reauth=1 network={ {% if eapol is defined and eapol is not none %} -{% if eapol.ca_cert_file is defined and eapol.ca_cert_file is not none %} - ca_cert="{{ eapol.ca_cert_file }}" +{% if eapol.ca_certificate is defined and eapol.ca_certificate is not none %} + ca_cert="/run/wpa_supplicant/{{ ifname }}_ca.pem" {% endif %} - client_cert="{{ eapol.cert_file }}" - private_key="{{ eapol.key_file }}" + client_cert="/run/wpa_supplicant/{{ ifname }}_cert.pem" + private_key="/run/wpa_supplicant/{{ ifname }}_cert.key" {% endif %} # list of accepted authenticated key management protocols diff --git a/data/templates/firewall/nftables-vrf-zones.tmpl b/data/templates/firewall/nftables-vrf-zones.tmpl new file mode 100644 index 000000000..eecf47b78 --- /dev/null +++ b/data/templates/firewall/nftables-vrf-zones.tmpl @@ -0,0 +1,17 @@ +table inet vrf_zones { + # Map of interfaces and connections tracking zones + map ct_iface_map { + typeof iifname : ct zone + } + # Assign unique zones for each VRF + # Chain for inbound traffic + chain vrf_zones_ct_in { + type filter hook prerouting priority raw; policy accept; + counter ct zone set iifname map @ct_iface_map + } + # Chain for locally-generated traffic + chain vrf_zones_ct_out { + type filter hook output priority raw; policy accept; + counter ct zone set oifname map @ct_iface_map + } +} diff --git a/data/templates/frr/bgpd.frr.tmpl b/data/templates/frr/bgpd.frr.tmpl index c21e7f234..aa297876b 100644 --- a/data/templates/frr/bgpd.frr.tmpl +++ b/data/templates/frr/bgpd.frr.tmpl @@ -65,6 +65,9 @@ {% if config.shutdown is defined %} neighbor {{ neighbor }} shutdown {% endif %} +{% if config.solo is defined %} + neighbor {{ neighbor }} solo +{% endif %} {% if config.strict_capability_match is defined %} neighbor {{ neighbor }} strict-capability-match {% endif %} diff --git a/data/templates/frr/isis.frr.tmpl b/data/templates/frr/isisd.frr.tmpl index 1e651898b..6cfa076d0 100644 --- a/data/templates/frr/isis.frr.tmpl +++ b/data/templates/frr/isisd.frr.tmpl @@ -116,18 +116,33 @@ router isis VyOS {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% endfor %} {% endfor %} {% endif %} -{% if redistribute is defined and redistribute.ipv4 is defined and redistribute.ipv4 is not none %} -{% for protocol in redistribute.ipv4 %} -{% for level, level_config in redistribute.ipv4[protocol].items() %} -{% if level_config.metric is defined and level_config.metric is not none %} +{% if redistribute is defined %} +{% if redistribute.ipv4 is defined and redistribute.ipv4 is not none %} +{% for protocol, protocol_options in redistribute.ipv4.items() %} +{% for level, level_config in protocol_options.items() %} +{% if level_config.metric is defined and level_config.metric is not none %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }} -{% elif level_config.route_map is defined and level_config.route_map is not none %} +{% elif level_config.route_map is defined and level_config.route_map is not none %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }} -{% else %} +{% else %} redistribute ipv4 {{ protocol }} {{ level | replace('_', '-') }} -{% endif %} +{% endif %} +{% endfor %} {% endfor %} -{% endfor %} +{% endif %} +{% if redistribute.ipv6 is defined and redistribute.ipv6 is not none %} +{% for protocol, protocol_options in redistribute.ipv6.items() %} +{% for level, level_config in protocol_options.items() %} +{% if level_config.metric is defined and level_config.metric is not none %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} metric {{ level_config.metric }} +{% elif level_config.route_map is defined and level_config.route_map is not none %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} route-map {{ level_config.route_map }} +{% else %} + redistribute ipv6 {{ protocol }} {{ level | replace('_', '-') }} +{% endif %} +{% endfor %} +{% endfor %} +{% endif %} {% endif %} {% if level is defined and level is not none %} {% if level == 'level-2' %} @@ -180,4 +195,4 @@ interface {{ iface }} {{ 'vrf ' + vrf if vrf is defined and vrf is not none }} {% endif %} {% endfor %} {% endif %} -! +!
\ No newline at end of file diff --git a/data/templates/https/nginx.default.tmpl b/data/templates/https/nginx.default.tmpl index 916764410..2f8aa06c2 100644 --- a/data/templates/https/nginx.default.tmpl +++ b/data/templates/https/nginx.default.tmpl @@ -17,7 +17,7 @@ server { listen {{ server.port }} ssl; listen [::]:{{ server.port }} ssl; {% else %} - listen {{ server.address }}:{{ server.port }} ssl; + listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl; {% endif %} {% for name in server.name %} @@ -30,7 +30,8 @@ server { include {{ server.certbot_dir }}/options-ssl-nginx.conf; ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem; {% elif server.vyos_cert %} - include {{ server.vyos_cert.conf }}; + ssl_certificate {{ server.vyos_cert.crt }}; + ssl_certificate_key {{ server.vyos_cert.key }}; {% else %} # # Self signed certs generated by the ssl-cert package @@ -40,7 +41,7 @@ server { {% endif %} # proxy settings for HTTP API, if enabled; 503, if not - location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc) { + location ~ /(retrieve|configure|config-file|image|generate|show|docs|openapi.json|redoc|graphql) { {% if server.api %} proxy_pass http://localhost:{{ server.api.port }}; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; diff --git a/data/templates/ipsec/charon/dhcp.conf.tmpl b/data/templates/ipsec/charon/dhcp.conf.tmpl index 2879550a8..92774b275 100644 --- a/data/templates/ipsec/charon/dhcp.conf.tmpl +++ b/data/templates/ipsec/charon/dhcp.conf.tmpl @@ -1,12 +1,11 @@ dhcp { load = yes - -{% if options is defined and options.remote_access is defined and options.remote_access.dhcp_pool is defined %} -{% if options.remote_access.dhcp_pool.interface is defined %} - interface = {{ options.remote_access.dhcp_pool.interface }} +{% if remote_access is defined and remote_access.dhcp is defined %} +{% if remote_access.dhcp.interface is defined %} + interface = {{ remote_access.dhcp.interface }} {% endif %} -{% if options.remote_access.dhcp_pool.server is defined %} - server = {{ options.remote_access.dhcp_pool.server }} +{% if remote_access.dhcp.server is defined %} + server = {{ remote_access.dhcp.server }} {% endif %} {% endif %} diff --git a/data/templates/ipsec/charon/eap-radius.conf.tmpl b/data/templates/ipsec/charon/eap-radius.conf.tmpl new file mode 100644 index 000000000..5ec35c988 --- /dev/null +++ b/data/templates/ipsec/charon/eap-radius.conf.tmpl @@ -0,0 +1,115 @@ +eap-radius { + # Send RADIUS accounting information to RADIUS servers. + # accounting = no + + # Close the IKE_SA if there is a timeout during interim RADIUS accounting + # updates. + # accounting_close_on_timeout = yes + + # Interval in seconds for interim RADIUS accounting updates, if not + # specified by the RADIUS server in the Access-Accept message. + # accounting_interval = 0 + + # If enabled, accounting is disabled unless an IKE_SA has at least one + # virtual IP. Only for IKEv2, for IKEv1 a virtual IP is strictly necessary. + # accounting_requires_vip = no + + # If enabled, adds the Class attributes received in Access-Accept message to + # the RADIUS accounting messages. + # accounting_send_class = no + + # Use class attributes in Access-Accept messages as group membership + # information. + # class_group = no + + # Closes all IKE_SAs if communication with the RADIUS server times out. If + # it is not set only the current IKE_SA is closed. + # close_all_on_timeout = no + + # Send EAP-Start instead of EAP-Identity to start RADIUS conversation. + # eap_start = no + + # Use filter_id attribute as group membership information. + # filter_id = no + + # Prefix to EAP-Identity, some AAA servers use a IMSI prefix to select the + # EAP method. + # id_prefix = + + # Whether to load the plugin. Can also be an integer to increase the + # priority of this plugin. + load = yes + + # NAS-Identifier to include in RADIUS messages. + nas_identifier = {{ remote_access.radius.nas_identifier if remote_access is defined and remote_access.radius is defined and remote_access.radius.nas_identifier is defined else 'strongSwan' }} + + # Port of RADIUS server (authentication). + # port = 1812 + + # Base to use for calculating exponential back off. + # retransmit_base = 1.4 + + # Timeout in seconds before sending first retransmit. + # retransmit_timeout = 2.0 + + # Number of times to retransmit a packet before giving up. + # retransmit_tries = 4 + + # Shared secret between RADIUS and NAS. If set, make sure to adjust the + # permissions of the config file accordingly. + # secret = + + # IP/Hostname of RADIUS server. + # server = + + # Number of sockets (ports) to use, increase for high load. + # sockets = 1 + + # Whether to include the UDP port in the Called- and Calling-Station-Id + # RADIUS attributes. + # station_id_with_port = yes + + dae { + # Enables support for the Dynamic Authorization Extension (RFC 5176). + # enable = no + + # Address to listen for DAE messages from the RADIUS server. + # listen = 0.0.0.0 + + # Port to listen for DAE requests. + # port = 3799 + + # Shared secret used to verify/sign DAE messages. If set, make sure to + # adjust the permissions of the config file accordingly. + # secret = + } + + forward { + # RADIUS attributes to be forwarded from IKEv2 to RADIUS. + # ike_to_radius = + + # Same as ike_to_radius but from RADIUS to IKEv2. + # radius_to_ike = + } + + # Section to specify multiple RADIUS servers. + servers { +{% if remote_access is defined and remote_access.radius is defined and remote_access.radius.server is defined %} +{% for server, server_options in remote_access.radius.server.items() if server_options.disable is not defined %} + {{ server | replace('.', '-') }} { + address = {{ server }} + secret = {{ server_options.key }} + auth_port = {{ server_options.port }} +{% if server_options.disable_accounting is not defined %} + acct_port = {{ server_options.port | int +1 }} +{% endif %} + sockets = 20 + } +{% endfor %} +{% endif %} + } + + # Section to configure multiple XAuth authentication rounds via RADIUS. + xauth { + } +} diff --git a/data/templates/ipsec/interfaces_use.conf.tmpl b/data/templates/ipsec/interfaces_use.conf.tmpl index 3d285b9be..a77102396 100644 --- a/data/templates/ipsec/interfaces_use.conf.tmpl +++ b/data/templates/ipsec/interfaces_use.conf.tmpl @@ -1,6 +1,5 @@ -{% if ipsec_interfaces is defined and 'interface' in ipsec_interfaces %} -{% set interfaces = ipsec_interfaces['interface'] %} +{% if interface is defined %} charon { - interfaces_use = {{ ', '.join(interfaces) if interfaces is not string else interfaces }} + interfaces_use = {{ ', '.join(interface) }} } {% endif %}
\ No newline at end of file diff --git a/data/templates/ipsec/ios_profile.tmpl b/data/templates/ipsec/ios_profile.tmpl new file mode 100644 index 000000000..af6c79d6e --- /dev/null +++ b/data/templates/ipsec/ios_profile.tmpl @@ -0,0 +1,104 @@ +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <!-- Set the name to whatever you like, it is used in the profile list on the device --> + <key>PayloadDisplayName</key> + <string>{{ profile_name }}</string> + <!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles --> + <key>PayloadIdentifier</key> + <string>{{ rfqdn }}</string> + <!-- A globally unique identifier, use uuidgen on Linux/Mac OS X to generate it --> + <key>PayloadUUID</key> + <string>{{ '' | get_uuid }}</string> + <key>PayloadType</key> + <string>Configuration</string> + <key>PayloadVersion</key> + <integer>1</integer> + <key>PayloadContent</key> + <array> + <!-- It is possible to add multiple VPN payloads with different identifiers/UUIDs and names --> + <dict> + <!-- This is an extension of the identifier given above --> + <key>PayloadIdentifier</key> + <string>{{ rfqdn }}.conf1</string> + <!-- A globally unique identifier for this payload --> + <key>PayloadUUID</key> + <string>{{ '' | get_uuid }}</string> + <key>PayloadType</key> + <string>com.apple.vpn.managed</string> + <key>PayloadVersion</key> + <integer>1</integer> + <!-- This is the name of the VPN connection as seen in the VPN application later --> + <key>UserDefinedName</key> + <string>{{ vpn_name }}</string> + <key>VPNType</key> + <string>IKEv2</string> + <key>IKEv2</key> + <dict> + <!-- Hostname or IP address of the VPN server --> + <key>RemoteAddress</key> + <string>{{ remote }}</string> + <!-- Remote identity, can be a FQDN, a userFQDN, an IP or (theoretically) a certificate's subject DN. Can't be empty. + IMPORTANT: DNs are currently not handled correctly, they are always sent as identities of type FQDN --> + <key>RemoteIdentifier</key> + <string>{{ authentication.id if authentication.id is defined else 'fooo' }}</string> + <!-- Local IKE identity, same restrictions as above. If it is empty the client's IP address will be used --> + <key>LocalIdentifier</key> + <string></string> + <!-- Optional, if it matches the CN of the root CA certificate (not the full subject DN) a certificate request will be sent + NOTE: If this is not configured make sure to configure leftsendcert=always on the server, otherwise it won't send its certificate --> + <key>ServerCertificateIssuerCommonName</key> + <string>{{ ca_cn }}</string> + <!-- Optional, the CN or one of the subjectAltNames of the server certificate to verify it, if not set RemoteIdentifier will be used --> + <key>ServerCertificateCommonName</key> + <string>{{ cert_cn }}</string> + <!-- The server is authenticated using a certificate --> + <key>AuthenticationMethod</key> + <string>Certificate</string> + <!-- The client uses EAP to authenticate --> + <key>ExtendedAuthEnabled</key> + <integer>1</integer> + <!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES. + IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration --> + <key>IKESecurityAssociationParameters</key> + <dict> + <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2encryptionalgorithm --> + <key>EncryptionAlgorithm</key> + <string>{{ ike_encryption.encryption }}</string> + <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2integrityalgorithm --> + <key>IntegrityAlgorithm</key> + <string>{{ ike_encryption.hash }}</string> + <!-- @see https://developer.apple.com/documentation/networkextension/nevpnikev2diffiehellmangroup --> + <key>DiffieHellmanGroup</key> + <integer>{{ ike_encryption.dh_group }}</integer> + </dict> + <key>ChildSecurityAssociationParameters</key> + <dict> + <key>EncryptionAlgorithm</key> + <string>{{ esp_encryption.encryption }}</string> + <key>IntegrityAlgorithm</key> + <string>{{ esp_encryption.hash }}</string> + <key>DiffieHellmanGroup</key> + <integer>{{ ike_encryption.dh_group }}</integer> + </dict> + </dict> + </dict> + <!-- This payload is optional but it provides an easy way to install the CA certificate together with the configuration --> + <dict> + <key>PayloadIdentifier</key> + <string>org.example.ca</string> + <key>PayloadUUID</key> + <string>{{ '' | get_uuid }}</string> + <key>PayloadType</key> + <string>com.apple.security.root</string> + <key>PayloadVersion</key> + <integer>1</integer> + <!-- This is the Base64 (PEM) encoded CA certificate --> + <key>PayloadContent</key> + <data> + {{ ca_cert }} + </data> + </dict> + </array> +</dict> +</plist> diff --git a/data/templates/ipsec/ipsec.conf.tmpl b/data/templates/ipsec/ipsec.conf.tmpl index a9ea1aac7..1cb531e76 100644 --- a/data/templates/ipsec/ipsec.conf.tmpl +++ b/data/templates/ipsec/ipsec.conf.tmpl @@ -16,9 +16,3 @@ config setup {% if include_ipsec_conf is defined %} include {{ include_ipsec_conf }} {% endif %} - -{% if delim_ipsec_l2tp_begin is defined %} -{{delim_ipsec_l2tp_begin}} -include {{ipsec_ra_conn_file}} -{{delim_ipsec_l2tp_end}} -{% endif %} diff --git a/data/templates/ipsec/ipsec.secrets.tmpl b/data/templates/ipsec/ipsec.secrets.tmpl index 43b5fe0d2..057e291ed 100644 --- a/data/templates/ipsec/ipsec.secrets.tmpl +++ b/data/templates/ipsec/ipsec.secrets.tmpl @@ -3,13 +3,3 @@ {% if include_ipsec_secrets is defined %} include {{ include_ipsec_secrets }} {% endif %} - -{% if delim_ipsec_l2tp_begin is defined %} -{{delim_ipsec_l2tp_begin}} -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} -{{outside_addr}} %any : PSK "{{ipsec_l2tp_secret}}" -{% elif ipsec_l2tp_auth_mode == 'x509' %} -: RSA {{server_key_file_copied}} -{% endif %} -{{delim_ipsec_l2tp_end}} -{% endif %} diff --git a/data/templates/ipsec/remote-access.tmpl b/data/templates/ipsec/remote-access.tmpl deleted file mode 100644 index fae48232f..000000000 --- a/data/templates/ipsec/remote-access.tmpl +++ /dev/null @@ -1,28 +0,0 @@ -{{delim_ipsec_l2tp_begin}} -conn {{ra_conn_name}} - type=transport - left={{outside_addr}} - leftsubnet=%dynamic[/1701] - rightsubnet=%dynamic - mark_in=%unique - auto=add - ike=aes256-sha1-modp1024,3des-sha1-modp1024,3des-sha1-modp1024! - dpddelay=15 - dpdtimeout=45 - dpdaction=clear - esp=aes256-sha1,3des-sha1! - rekey=no -{% if ipsec_l2tp_auth_mode == 'pre-shared-secret' %} - authby=secret - leftauth=psk - rightauth=psk -{% elif ipsec_l2tp_auth_mode == 'x509' %} - authby=rsasig - leftrsasigkey=%cert - rightrsasigkey=%cert - rightca=%same - leftcert={{server_cert_file_copied}} -{% endif %} - ikelifetime={{ipsec_l2tp_ike_lifetime}} - keylife={{ipsec_l2tp_lifetime}} -{{delim_ipsec_l2tp_end}} diff --git a/data/templates/ipsec/swanctl.conf.tmpl b/data/templates/ipsec/swanctl.conf.tmpl index a6ab73cc2..161f19f95 100644 --- a/data/templates/ipsec/swanctl.conf.tmpl +++ b/data/templates/ipsec/swanctl.conf.tmpl @@ -1,4 +1,5 @@ ### Autogenerated by vpn_ipsec.py ### +{% import 'ipsec/swanctl/l2tp.tmpl' as l2tp_tmpl %} {% import 'ipsec/swanctl/profile.tmpl' as profile_tmpl %} {% import 'ipsec/swanctl/peer.tmpl' as peer_tmpl %} {% import 'ipsec/swanctl/remote_access.tmpl' as remote_access_tmpl %} @@ -14,21 +15,28 @@ connections { {{ peer_tmpl.conn(peer, peer_conf, ike_group, esp_group) }} {% endfor %} {% endif %} -{% if remote_access is defined and remote_access is not none %} -{% for rw, rw_conf in remote_access.items() if rw_conf.disable is not defined %} +{% if remote_access is defined and remote_access.connection is defined and remote_access.connection is not none %} +{% for rw, rw_conf in remote_access.connection.items() if rw_conf.disable is not defined %} {{ remote_access_tmpl.conn(rw, rw_conf, ike_group, esp_group) }} {% endfor %} {% endif %} +{% if l2tp %} +{{ l2tp_tmpl.conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) }} +{% endif %} } pools { -{% if remote_access is defined %} -{% for ra, ra_conf in remote_access.items() if ra_conf.pool.dhcp_enable is not defined %} - ra-{{ ra }} { - addrs = {{ ra_conf.pool.prefix }} - dns = {{ ra_conf.pool.name_server | join(",") }} -{% if ra_conf.pool.exclude is defined %} - split_exclude = {{ ra_conf.pool.exclude | join(",") }} +{% if remote_access is defined and remote_access.pool is defined and remote_access.pool is not none %} +{% for pool, pool_config in remote_access.pool.items() %} + {{ pool }} { +{% if pool_config.prefix is defined and pool_config.prefix is not none %} + addrs = {{ pool_config.prefix }} +{% endif %} +{% if pool_config.name_server is defined and pool_config.name_server is not none %} + dns = {{ pool_config.name_server | join(',') }} +{% endif %} +{% if pool_config.exclude is defined and pool_config.exclude is not none %} + split_exclude = {{ pool_config.exclude | join(',') }} {% endif %} } {% endfor %} @@ -81,8 +89,8 @@ secrets { {% endif %} {% endfor %} {% endif %} -{% if remote_access is defined %} -{% for ra, ra_conf in remote_access.items() if remote_access is defined %} +{% if remote_access is defined and remote_access.connection is defined and remote_access.connection is not none %} +{% for ra, ra_conf in remote_access.connection.items() if ra_conf.disable is not defined %} {% if ra_conf.authentication.server_mode == 'pre-shared-secret' %} ike_{{ ra }} { {% if ra_conf.authentication.id is defined %} @@ -103,5 +111,21 @@ secrets { {% endif %} {% endfor %} {% endif %} +{% if l2tp %} +{% if l2tp.authentication.mode == 'pre-shared-secret' %} + ike_l2tp_remote_access { + id = "{{ l2tp_outside_address }}" + secret = "{{ l2tp.authentication.pre_shared_secret }}" + } +{% elif l2tp.authentication.mode == 'x509' %} + private_l2tp_remote_access { + id = "{{ l2tp_outside_address }}" + file = {{ l2tp.authentication.x509.certificate }}.pem +{% if l2tp.authentication.x509.passphrase is defined %} + secret = "{{ l2tp.authentication.x509.passphrase }}" +{% endif %} + } +{% endif %} +{% endif %} } diff --git a/data/templates/ipsec/swanctl/l2tp.tmpl b/data/templates/ipsec/swanctl/l2tp.tmpl new file mode 100644 index 000000000..2df5c2a4d --- /dev/null +++ b/data/templates/ipsec/swanctl/l2tp.tmpl @@ -0,0 +1,30 @@ +{% macro conn(l2tp, l2tp_outside_address, l2tp_ike_default, l2tp_esp_default, ike_group, esp_group) %} +{% set l2tp_ike = ike_group[l2tp.ike_group] if l2tp.ike_group is defined else None %} +{% set l2tp_esp = esp_group[l2tp.esp_group] if l2tp.esp_group is defined else None %} + l2tp_remote_access { + proposals = {{ l2tp_ike | get_esp_ike_cipher | join(',') if l2tp_ike else l2tp_ike_default }} + local_addrs = {{ l2tp_outside_address }} + dpd_delay = 15s + dpd_timeout = 45s + rekey_time = {{ l2tp_ike.lifetime if l2tp_ike else l2tp.ike_lifetime }}s + reauth_time = 0 + local { + auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }} +{% if l2tp.authentication.mode == 'x509' %} + certs = {{ l2tp.authentication.x509.certificate }}.pem +{% endif %} + } + remote { + auth = {{ 'psk' if l2tp.authentication.mode == 'pre-shared-secret' else 'pubkey' }} + } + children { + l2tp_remote_access_esp { + mode = transport + esp_proposals = {{ l2tp_esp | get_esp_ike_cipher | join(',') if l2tp_esp else l2tp_esp_default }} + life_time = {{ l2tp_esp.lifetime if l2tp_esp else l2tp.lifetime }}s + local_ts = dynamic[/1701] + remote_ts = dynamic + } + } + } +{% endmacro %} diff --git a/data/templates/ipsec/swanctl/peer.tmpl b/data/templates/ipsec/swanctl/peer.tmpl index 8e46e8892..dd29ea7d4 100644 --- a/data/templates/ipsec/swanctl/peer.tmpl +++ b/data/templates/ipsec/swanctl/peer.tmpl @@ -54,7 +54,7 @@ } children { {% if peer_conf.vti is defined and peer_conf.vti.bind is defined and peer_conf.tunnel is not defined %} -{% set vti_esp = esp_group[peer_conf.vti.esp_group] if peer_conf.vti.esp_group is defined else None %} +{% set vti_esp = esp_group[ peer_conf.vti.esp_group ] if peer_conf.vti.esp_group is defined else esp_group[ peer_conf.default_esp_group ] %} peer_{{ name }}_vti { esp_proposals = {{ vti_esp | get_esp_ike_cipher | join(',') }} local_ts = 0.0.0.0/0,::/0 @@ -86,7 +86,7 @@ {% set remote_port = tunnel_conf.remote.port if tunnel_conf.remote is defined and tunnel_conf.remote.port is defined else '' %} {% set remote_suffix = '[{0}/{1}]'.format(proto, remote_port) if proto or remote_port else '' %} peer_{{ name }}_tunnel_{{ tunnel_id }} { - esp_proposals = {{ esp_group[peer_conf.default_esp_group] | get_esp_ike_cipher | join(',') }} + esp_proposals = {{ tunnel_esp | get_esp_ike_cipher | join(',') }} {% if tunnel_esp.mode is not defined or tunnel_esp.mode == 'tunnel' %} {% if tunnel_conf.local is defined and tunnel_conf.local.prefix is defined %} {% set local_prefix = tunnel_conf.local.prefix if 'any' not in tunnel_conf.local.prefix else ['0.0.0.0/0', '::/0'] %} diff --git a/data/templates/ipsec/swanctl/remote_access.tmpl b/data/templates/ipsec/swanctl/remote_access.tmpl index 95f2108fb..456842488 100644 --- a/data/templates/ipsec/swanctl/remote_access.tmpl +++ b/data/templates/ipsec/swanctl/remote_access.tmpl @@ -10,10 +10,9 @@ send_certreq = no rekey_time = {{ ike.lifetime }}s keyingtries = 0 -{% if rw_conf.pool.dhcp_enable is defined %} - pools = dhcp -{% else %} - pools = ra-{{ name }} + unique = {{ rw_conf.unique }} +{% if rw_conf.pool is defined and rw_conf.pool is not none %} + pools = {{ rw_conf.pool | join(',') }} {% endif %} local { {% if rw_conf.authentication.id is defined and rw_conf.authentication.use_x509_id is not defined %} @@ -34,7 +33,7 @@ } children { ikev2-vpn { - esp_proposals = {{ esp | get_esp_ike_cipher | join(',') }} + esp_proposals = {{ esp | get_esp_ike_cipher | join(',') }} rekey_time = {{ esp.lifetime }}s rand_time = 540s dpd_action = clear diff --git a/data/templates/ipsec/windows_profile.tmpl b/data/templates/ipsec/windows_profile.tmpl new file mode 100644 index 000000000..8c26944be --- /dev/null +++ b/data/templates/ipsec/windows_profile.tmpl @@ -0,0 +1,4 @@ +Remove-VpnConnection -Name "{{ vpn_name }}" -Force -PassThru + +Add-VpnConnection -Name "{{ vpn_name }}" -ServerAddress "{{ remote }}" -TunnelType "Ikev2" +Set-VpnConnectionIPsecConfiguration -ConnectionName "{{ vpn_name }}" -AuthenticationTransformConstants {{ ike_encryption.encryption }} -CipherTransformConstants {{ ike_encryption.encryption }} -EncryptionMethod {{ esp_encryption.encryption }} -IntegrityCheckMethod {{ esp_encryption.hash }} -PfsGroup None -DHGroup "Group{{ ike_encryption.dh_group }}" -PassThru -Force diff --git a/data/templates/ocserv/ocserv_config.tmpl b/data/templates/ocserv/ocserv_config.tmpl index 328af0c0d..0be805235 100644 --- a/data/templates/ocserv/ocserv_config.tmpl +++ b/data/templates/ocserv/ocserv_config.tmpl @@ -12,16 +12,16 @@ auth = "radius [config=/run/ocserv/radiusclient.conf]" auth = "plain[/run/ocserv/ocpasswd]" {% endif %} -{% if ssl.cert_file %} -server-cert = {{ ssl.cert_file }} +{% if ssl.certificate is defined %} +server-cert = /run/ocserv/cert.pem +server-key = /run/ocserv/cert.key +{% if ssl.passphrase is defined %} +key-pin = {{ ssl.passphrase }} {% endif %} - -{% if ssl.key_file %} -server-key = {{ ssl.key_file }} {% endif %} -{% if ssl.ca_cert_file %} -ca-cert = {{ ssl.ca_cert_file }} +{% if ssl.ca_certificate is defined %} +ca-cert = /run/ocserv/ca.pem {% endif %} socket-file = /run/ocserv/ocserv.socket diff --git a/data/templates/openvpn/server.conf.tmpl b/data/templates/openvpn/server.conf.tmpl index c5d665c0b..d9f01310e 100644 --- a/data/templates/openvpn/server.conf.tmpl +++ b/data/templates/openvpn/server.conf.tmpl @@ -36,8 +36,8 @@ rport {{ remote_port }} remote {{ remote }} {% endfor %} {% endif %} -{% if shared_secret_key_file is defined and shared_secret_key_file is not none %} -secret {{ shared_secret_key_file }} +{% if shared_secret_key is defined and shared_secret_key is not none %} +secret /run/openvpn/{{ ifname }}_shared.key {% endif %} {% if persistent_tunnel is defined %} persist-tun @@ -157,32 +157,32 @@ ifconfig-ipv6 {{ laddr }} {{ raddr }} {% if tls is defined and tls is not none %} # TLS options -{% if tls.ca_cert_file is defined and tls.ca_cert_file is not none %} -ca {{ tls.ca_cert_file }} +{% if tls.ca_certificate is defined and tls.ca_certificate is not none %} +ca /run/openvpn/{{ ifname }}_ca.pem {% endif %} -{% if tls.cert_file is defined and tls.cert_file is not none %} -cert {{ tls.cert_file }} +{% if tls.certificate is defined and tls.certificate is not none %} +cert /run/openvpn/{{ ifname }}_cert.pem {% endif %} -{% if tls.key_file is defined and tls.key_file is not none %} -key {{ tls.key_file }} +{% if tls.private_key is defined %} +key /run/openvpn/{{ ifname }}_cert.key {% endif %} -{% if tls.crypt_file is defined and tls.crypt_file is not none %} -tls-crypt {{ tls.crypt_file }} +{% if tls.crypt_key is defined and tls.crypt_key is not none %} +tls-crypt /run/openvpn/{{ ifname }}_crypt.key {% endif %} -{% if tls.crl_file is defined and tls.crl_file is not none %} -crl-verify {{ tls.crl_file }} +{% if tls.crl is defined %} +crl-verify /run/openvpn/{{ ifname }}_crl.pem {% endif %} {% if tls.tls_version_min is defined and tls.tls_version_min is not none %} tls-version-min {{ tls.tls_version_min }} {% endif %} -{% if tls.dh_file is defined and tls.dh_file is not none %} -dh {{ tls.dh_file }} +{% if tls.dh_params is defined and tls.dh_params is not none %} +dh /run/openvpn/{{ ifname }}_dh.pem {% endif %} -{% if tls.auth_file is defined and tls.auth_file is not none %} +{% if tls.auth_key is defined and tls.auth_key is not none %} {% if mode == 'client' %} -tls-auth {{ tls.auth_file }} 1 +tls-auth /run/openvpn/{{ ifname }}_auth.key 1 {% elif mode == 'server' %} -tls-auth {{ tls.auth_file }} 0 +tls-auth /run/openvpn/{{ ifname }}_auth.key 0 {% endif %} {% endif %} {% if tls.role is defined and tls.role is not none %} @@ -197,56 +197,15 @@ tls-server # Encryption options {% if encryption is defined and encryption is not none %} {% if encryption.cipher is defined and encryption.cipher is not none %} -{% if encryption.cipher == 'none' %} -cipher none -{% elif encryption.cipher == 'des' %} -cipher des-cbc -{% elif encryption.cipher == '3des' %} -cipher des-ede3-cbc -{% elif encryption.cipher == 'bf128' %} -cipher bf-cbc +cipher {{ encryption.cipher | openvpn_cipher }} +{% if encryption.cipher == 'bf128' %} keysize 128 {% elif encryption.cipher == 'bf256' %} -cipher bf-cbc -keysize 25 -{% elif encryption.cipher == 'aes128gcm' %} -cipher aes-128-gcm -{% elif encryption.cipher == 'aes128' %} -cipher aes-128-cbc -{% elif encryption.cipher == 'aes192gcm' %} -cipher aes-192-gcm -{% elif encryption.cipher == 'aes192' %} -cipher aes-192-cbc -{% elif encryption.cipher == 'aes256gcm' %} -cipher aes-256-gcm -{% elif encryption.cipher == 'aes256' %} -cipher aes-256-cbc +keysize 256 {% endif %} {% endif %} {% if encryption.ncp_ciphers is defined and encryption.ncp_ciphers is not none %} -{% set cipher_list = [] %} -{% for cipher in encryption.ncp_ciphers %} -{% if cipher == 'none' %} -{% set cipher_list = cipher_list.append('none') %} -{% elif cipher == 'des' %} -{% set cipher_list = cipher_list.append('des-cbc') %} -{% elif cipher == '3des' %} -{% set cipher_list = cipher_list.append('des-ede3-cbc') %} -{% elif cipher == 'aes128' %} -{% set cipher_list = cipher_list.append('aes-128-cbc') %} -{% elif cipher == 'aes128gcm' %} -{% set cipher_list = cipher_list.append('aes-128-gcm') %} -{% elif cipher == 'aes192' %} -{% set cipher_list = cipher_list.append('aes-192-cbc') %} -{% elif cipher == 'aes192gcm' %} -{% set cipher_list = cipher_list.append('aes-192-gcm') %} -{% elif cipher == 'aes256' %} -{% set cipher_list = cipher_list.append('aes-256-cbc') %} -{% elif cipher == 'aes256gcm' %} -{% set cipher_list = cipher_list.append('aes-256-gcm') %} -{% endif %} -{% endfor %} -ncp-ciphers {{ cipher_list | join(':') }}:{{ cipher_list | join(':') | upper }} +data-ciphers {{ encryption.ncp_ciphers | openvpn_ncp_ciphers }} {% endif %} {% endif %} diff --git a/data/templates/router-advert/radvd.conf.tmpl b/data/templates/router-advert/radvd.conf.tmpl index 9cc237512..88d066491 100644 --- a/data/templates/router-advert/radvd.conf.tmpl +++ b/data/templates/router-advert/radvd.conf.tmpl @@ -1,58 +1,64 @@ ### Autogenerated by service_router-advert.py ### {% if interface is defined and interface is not none %} -{% for iface in interface %} +{% for iface, iface_config in interface.items() %} interface {{ iface }} { IgnoreIfMissing on; -{% if interface[iface].default_preference is defined and interface[iface].default_preference is not none %} - AdvDefaultPreference {{ interface[iface].default_preference }}; +{% if iface_config.default_preference is defined and iface_config.default_preference is not none %} + AdvDefaultPreference {{ iface_config.default_preference }}; {% endif %} -{% if interface[iface].managed_flag is defined and interface[iface].managed_flag is not none %} - AdvManagedFlag {{ 'on' if interface[iface].managed_flag is defined else 'off' }}; +{% if iface_config.managed_flag is defined and iface_config.managed_flag is not none %} + AdvManagedFlag {{ 'on' if iface_config.managed_flag is defined else 'off' }}; {% endif %} -{% if interface[iface].interval.max is defined and interface[iface].interval.max is not none %} - MaxRtrAdvInterval {{ interface[iface].interval.max }}; +{% if iface_config.interval.max is defined and iface_config.interval.max is not none %} + MaxRtrAdvInterval {{ iface_config.interval.max }}; {% endif %} -{% if interface[iface].interval.min is defined and interface[iface].interval.min is not none %} - MinRtrAdvInterval {{ interface[iface].interval.min }}; +{% if iface_config.interval.min is defined and iface_config.interval.min is not none %} + MinRtrAdvInterval {{ iface_config.interval.min }}; {% endif %} -{% if interface[iface].reachable_time is defined and interface[iface].reachable_time is not none %} - AdvReachableTime {{ interface[iface].reachable_time }}; +{% if iface_config.reachable_time is defined and iface_config.reachable_time is not none %} + AdvReachableTime {{ iface_config.reachable_time }}; {% endif %} - AdvIntervalOpt {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; - AdvSendAdvert {{ 'off' if interface[iface].no_send_advert is defined else 'on' }}; -{% if interface[iface].default_lifetime is defined %} - AdvDefaultLifetime {{ interface[iface].default_lifetime }}; -{% endif %} -{% if interface[iface].link_mtu is defined %} - AdvLinkMTU {{ interface[iface].link_mtu }}; -{% endif %} - AdvOtherConfigFlag {{ 'on' if interface[iface].other_config_flag is defined else 'off' }}; - AdvRetransTimer {{ interface[iface].retrans_timer }}; - AdvCurHopLimit {{ interface[iface].hop_limit }}; -{% if interface[iface].route is defined %} -{% for route in interface[iface].route %} + AdvIntervalOpt {{ 'off' if iface_config.no_send_advert is defined else 'on' }}; + AdvSendAdvert {{ 'off' if iface_config.no_send_advert is defined else 'on' }}; +{% if iface_config.default_lifetime is defined %} + AdvDefaultLifetime {{ iface_config.default_lifetime }}; +{% endif %} +{% if iface_config.link_mtu is defined %} + AdvLinkMTU {{ iface_config.link_mtu }}; +{% endif %} + AdvOtherConfigFlag {{ 'on' if iface_config.other_config_flag is defined else 'off' }}; + AdvRetransTimer {{ iface_config.retrans_timer }}; + AdvCurHopLimit {{ iface_config.hop_limit }}; +{% if iface_config.route is defined %} +{% for route, route_options in iface_config.route.items() %} route {{ route }} { -{% if interface[iface].route[route].valid_lifetime is defined %} - AdvRouteLifetime {{ interface[iface].route[route].valid_lifetime }}; +{% if route_options.valid_lifetime is defined %} + AdvRouteLifetime {{ route_options.valid_lifetime }}; {% endif %} -{% if interface[iface].route[route].route_preference is defined %} - AdvRoutePreference {{ interface[iface].route[route].route_preference }}; +{% if route_options.route_preference is defined %} + AdvRoutePreference {{ route_options.route_preference }}; {% endif %} - RemoveRoute {{ 'off' if interface[iface].route[route].no_remove_route is defined else 'on' }}; + RemoveRoute {{ 'off' if route_options.no_remove_route is defined else 'on' }}; }; {% endfor %} {% endif %} -{% for prefix in interface[iface].prefix %} +{% if iface_config.prefix is defined and iface_config.prefix is not none %} +{% for prefix, prefix_options in iface_config.prefix.items() %} prefix {{ prefix }} { - AdvAutonomous {{ 'off' if interface[iface].prefix[prefix].no_autonomous_flag is defined else 'on' }}; - AdvValidLifetime {{ interface[iface].prefix[prefix].valid_lifetime }}; - AdvOnLink {{ 'off' if interface[iface].prefix[prefix].no_on_link_flag is defined else 'on' }}; - AdvPreferredLifetime {{ interface[iface].prefix[prefix].preferred_lifetime }}; + AdvAutonomous {{ 'off' if prefix_options.no_autonomous_flag is defined else 'on' }}; + AdvValidLifetime {{ prefix_options.valid_lifetime }}; + AdvOnLink {{ 'off' if prefix_options.no_on_link_flag is defined else 'on' }}; + AdvPreferredLifetime {{ prefix_options.preferred_lifetime }}; + }; +{% endfor %} +{% endif %} +{% if iface_config.name_server is defined %} + RDNSS {{ iface_config.name_server | join(" ") }} { }; -{% endfor %} -{% if interface[iface].name_server is defined %} - RDNSS {{ interface[iface].name_server | join(" ") }} { +{% endif %} +{% if iface_config.dnssl is defined %} + DNSSL {{ iface_config.dnssl | join(" ") }} { }; {% endif %} }; diff --git a/data/templates/snmp/override.conf.tmpl b/data/templates/snmp/override.conf.tmpl index 90c294ed0..2ac45a89f 100644 --- a/data/templates/snmp/override.conf.tmpl +++ b/data/templates/snmp/override.conf.tmpl @@ -1,4 +1,5 @@ {% set vrf_command = 'ip vrf exec ' + vrf + ' ' if vrf is defined else '' %} +{% set oid_route_table = ' ' if route_table is sameas true else '-I -ipCidrRouteTable,inetCidrRouteTable' %} [Unit] StartLimitIntervalSec=0 After=vyos-router.service @@ -7,7 +8,7 @@ After=vyos-router.service Environment= Environment="MIBDIRS=/usr/share/snmp/mibs:/usr/share/snmp/mibs/iana:/usr/share/snmp/mibs/ietf:/usr/share/vyos/mibs" ExecStart= -ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp -I -ipCidrRouteTable,inetCidrRouteTable -f -p /run/snmpd.pid +ExecStart={{vrf_command}}/usr/sbin/snmpd -LS0-5d -Lf /dev/null -u Debian-snmp -g Debian-snmp {{oid_route_table}} -f -p /run/snmpd.pid Restart=always RestartSec=10 diff --git a/debian/control b/debian/control index 9ac82cf03..c5cbeb7d4 100644 --- a/debian/control +++ b/debian/control @@ -51,6 +51,7 @@ Depends: etherwake, ethtool, fdisk, + fastnetmon [amd64], file, frr (>= 7.5), frr-pythontools, @@ -89,6 +90,7 @@ Depends: minisign, modemmanager, mtr-tiny, + ndisc6, ndppd, netplug, nftables (>= 0.9.3), diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 2ed25755f..7ca568eff 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -10,6 +10,7 @@ etc/sudoers.d etc/systemd etc/sysctl.d etc/udev +etc/update-motd.d etc/vyos lib/ opt/ diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index a17fe351c..4b4c4c13e 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -76,3 +76,10 @@ if [ ! -x $POSTCONFIG_SCRIPT ]; then EOF fi +# symlink destination is deleted during ISO assembly - this generates some noise +# when the system boots: systemd-sysv-generator[1881]: stat() failed on +# /etc/init.d/README, ignoring: No such file or directory. Thus we simply drop +# the file. +if [ -L /etc/init.d/README ]; then + rm -f /etc/init.d/README +fi diff --git a/interface-definitions/containers.xml.in b/interface-definitions/containers.xml.in index 6fc53c105..124b1f65e 100644 --- a/interface-definitions/containers.xml.in +++ b/interface-definitions/containers.xml.in @@ -3,6 +3,7 @@ <node name="container" owner="${vyos_conf_scripts_dir}/containers.py"> <properties> <help>Container applications</help> + <priority>1280</priority> </properties> <children> <tagNode name="name"> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index b613e30c1..b65a89b56 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -1,7 +1,7 @@ <?xml version="1.0"?> <!-- HTTPS configuration --> <interfaceDefinition> - <syntaxVersion component='https' version='2'></syntaxVersion> + <syntaxVersion component='https' version='3'></syntaxVersion> <node name="service"> <children> <node name="https" owner="${vyos_conf_scripts_dir}/https.py"> @@ -123,22 +123,7 @@ <help>TLS certificates</help> </properties> <children> - <node name="system-generated-certificate" owner="${vyos_conf_scripts_dir}/vyos_cert.py"> - <properties> - <help>Use an automatically generated self-signed certificate</help> - </properties> - <children> - <leafNode name="lifetime"> - <properties> - <help>Lifetime in days; default is 365</help> - <valueHelp> - <format>1-65535</format> - <description>Number of days</description> - </valueHelp> - </properties> - </leafNode> - </children> - </node> + #include <include/pki/certificate.xml.i> <node name="certbot" owner="${vyos_conf_scripts_dir}/le_cert.py"> <properties> <help>Request or apply a letsencrypt certificate for domain-name</help> diff --git a/interface-definitions/include/accel-ppp/radius-additions.xml.i b/interface-definitions/include/accel-ppp/radius-additions.xml.i index e65088c43..fdcff36bf 100644 --- a/interface-definitions/include/accel-ppp/radius-additions.xml.i +++ b/interface-definitions/include/accel-ppp/radius-additions.xml.i @@ -88,11 +88,7 @@ </properties> <defaultValue>3</defaultValue> </leafNode> - <leafNode name="nas-identifier"> - <properties> - <help>NAS-Identifier attribute sent to RADIUS</help> - </properties> - </leafNode> + #include <include/radius-nas-identifier.xml.i> <leafNode name="nas-ip-address"> <properties> <help>NAS-IP-Address attribute sent to RADIUS</help> diff --git a/interface-definitions/include/auth-local-users.xml.i b/interface-definitions/include/auth-local-users.xml.i new file mode 100644 index 000000000..8ef09554e --- /dev/null +++ b/interface-definitions/include/auth-local-users.xml.i @@ -0,0 +1,22 @@ +<!-- include start from auth-local-users.xml.i --> +<node name="local-users"> + <properties> + <help>Local user authentication</help> + </properties> + <children> + <tagNode name="username"> + <properties> + <help>Username used for authentication</help> + </properties> + <children> + #include <include/generic-disable-node.xml.i> + <leafNode name="password"> + <properties> + <help>Password used for authentication</help> + </properties> + </leafNode> + </children> + </tagNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i index 1673f25a5..aaa69e6c8 100644 --- a/interface-definitions/include/bgp/afi-l2vpn-common.xml.i +++ b/interface-definitions/include/bgp/afi-l2vpn-common.xml.i @@ -11,17 +11,6 @@ <valueless/> </properties> </leafNode> -<leafNode name="rd"> - <properties> - <help>Route Distinguisher</help> - <valueHelp> - <format>txt</format> - <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description> - </valueHelp> - <constraint> - <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex> - </constraint> - </properties> -</leafNode> +#include <include/bgp/route-distinguisher.xml.i> #include <include/bgp/route-target.xml.i> <!-- include end --> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index 37fc7259f..552e85aa4 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -372,18 +372,7 @@ </constraint> </properties> <children> - <leafNode name="rd"> - <properties> - <help>Route Distinguisher</help> - <valueHelp> - <format>txt</format> - <description>Route Distinguisher, asn:xxx</description> - </valueHelp> - <constraint> - <regex>^[0-9]{1,10}:[0-9]{1,5}$</regex> - </constraint> - </properties> - </leafNode> + #include <include/bgp/route-distinguisher.xml.i> <leafNode name="label"> <properties> <help>MPLS label value assigned to route</help> @@ -772,18 +761,7 @@ </constraint> </properties> <children> - <leafNode name="rd"> - <properties> - <help>Route Distinguisher</help> - <valueHelp> - <format>txt</format> - <description>Route Distinguisher, asn:xxx</description> - </valueHelp> - <constraint> - <regex>^[0-9]{1,10}:[0-9]{1,5}$</regex> - </constraint> - </properties> - </leafNode> + #include <include/bgp/route-distinguisher.xml.i> <leafNode name="label"> <properties> <help>MPLS label value assigned to route</help> @@ -1038,6 +1016,12 @@ </leafNode> #include <include/bgp/remote-as.xml.i> #include <include/bgp/neighbor-shutdown.xml.i> + <leafNode name="solo"> + <properties> + <help>Do not send back prefixes learned from the neighbor</help> + <valueless/> + </properties> + </leafNode> <leafNode name="strict-capability-match"> <properties> <help>Enable strict capability negotiation</help> diff --git a/interface-definitions/include/bgp/route-distinguisher.xml.i b/interface-definitions/include/bgp/route-distinguisher.xml.i new file mode 100644 index 000000000..fdfbe7076 --- /dev/null +++ b/interface-definitions/include/bgp/route-distinguisher.xml.i @@ -0,0 +1,14 @@ +<!-- include start from bgp/route-distinguisher.xml.i --> +<leafNode name="rd"> + <properties> + <help>Route Distinguisher</help> + <valueHelp> + <format>txt</format> + <description>Route Distinguisher, (x.x.x.x:yyy|xxxx:yyyy)</description> + </valueHelp> + <constraint> + <regex>^((25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)(\.(25[0-5]|2[0-4][0-9]|[1][0-9][0-9]|[1-9][0-9]|[0-9]?)){3}|[0-9]{1,10}):[0-9]{1,5}$</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/dhcp-interface.xml.i b/interface-definitions/include/dhcp-interface.xml.i new file mode 100644 index 000000000..939b45f15 --- /dev/null +++ b/interface-definitions/include/dhcp-interface.xml.i @@ -0,0 +1,15 @@ + <leafNode name="dhcp-interface"> + <properties> + <help>DHCP interface supplying next-hop IP address</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>DHCP interface name</description> + </valueHelp> + <constraint> + <validator name="interface-name"/> + </constraint> + </properties> + </leafNode> diff --git a/interface-definitions/include/interface/interface-eapol.xml.i b/interface-definitions/include/interface/interface-eapol.xml.i index 92b7a3f35..270ec5b13 100644 --- a/interface-definitions/include/interface/interface-eapol.xml.i +++ b/interface-definitions/include/interface/interface-eapol.xml.i @@ -4,9 +4,8 @@ <help>Extensible Authentication Protocol over Local Area Network</help> </properties> <children> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - #include <include/certificate-key.xml.i> + #include <include/pki/ca-certificate.xml.i> + #include <include/pki/certificate-key.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i new file mode 100644 index 000000000..af2669335 --- /dev/null +++ b/interface-definitions/include/ipsec/authentication-pre-shared-secret.xml.i @@ -0,0 +1,11 @@ +<!-- include start from ipsec/authentication-pre-shared-secret.xml.i --> +<leafNode name="pre-shared-secret"> + <properties> + <help>Pre-shared secret key</help> + <valueHelp> + <format>txt</format> + <description>Pre-shared secret key</description> + </valueHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 831d12694..af5a21f49 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -492,6 +492,61 @@ </node> </children> </node> + <node name="ipv6"> + <properties> + <help>Redistribute IPv6 routes</help> + </properties> + <children> + <node name="bgp"> + <properties> + <help>Redistribute BGP routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + <node name="connected"> + <properties> + <help>Redistribute connected routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + <node name="kernel"> + <properties> + <help>Redistribute kernel routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + <node name="ospf6"> + <properties> + <help>Redistribute OSPFv3 routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + <node name="ripng"> + <properties> + <help>Redistribute RIPng routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + <node name="static"> + <properties> + <help>Redistribute static routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-ipv6.xml.i> + </children> + </node> + </children> + </node> </children> </node> <leafNode name="set-attached-bit"> @@ -711,4 +766,4 @@ </children> </tagNode> #include <include/route-map.xml.i> -<!-- include end --> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/isis/redistribute-ipv6.xml.i b/interface-definitions/include/isis/redistribute-ipv6.xml.i new file mode 100644 index 000000000..7e679e38a --- /dev/null +++ b/interface-definitions/include/isis/redistribute-ipv6.xml.i @@ -0,0 +1,42 @@ +<!-- include start from isis/redistribute-ipv6.xml.i --> +<node name="level-1"> + <properties> + <help>Redistribute into level-1</help> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format>u32:0-16777215</format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + #include <include/route-map.xml.i> + </children> +</node> +<node name="level-2"> + <properties> + <help>Redistribute into level-2</help> + </properties> + <children> + <leafNode name="metric"> + <properties> + <help>Metric for redistributed routes</help> + <valueHelp> + <format>u32:0-16777215</format> + <description>ISIS default metric</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-16777215"/> + </constraint> + </properties> + </leafNode> + #include <include/route-map.xml.i> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/radius-nas-identifier.xml.i b/interface-definitions/include/radius-nas-identifier.xml.i new file mode 100644 index 000000000..8e6933cc0 --- /dev/null +++ b/interface-definitions/include/radius-nas-identifier.xml.i @@ -0,0 +1,7 @@ +<!-- include start from radius-nas-identifier.xml.i --> +<leafNode name="nas-identifier"> + <properties> + <help>NAS-Identifier attribute sent to RADIUS</help> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/static/static-route.xml.i b/interface-definitions/include/static/static-route.xml.i index 254ea3163..21babc015 100644 --- a/interface-definitions/include/static/static-route.xml.i +++ b/interface-definitions/include/static/static-route.xml.i @@ -31,21 +31,7 @@ </leafNode> </children> </node> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface supplying next-hop IP address</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <valueHelp> - <format>txt</format> - <description>DHCP interface name</description> - </valueHelp> - <constraint> - <validator name="interface-name"/> - </constraint> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <tagNode name="interface"> <properties> <help>Next-hop IPv4 router interface</help> diff --git a/interface-definitions/interfaces-openvpn.xml.in b/interface-definitions/interfaces-openvpn.xml.in index 681290570..7ff08ac86 100644 --- a/interface-definitions/interfaces-openvpn.xml.in +++ b/interface-definitions/interfaces-openvpn.xml.in @@ -637,16 +637,12 @@ </leafNode> </children> </node> - <leafNode name="shared-secret-key-file"> + <leafNode name="shared-secret-key"> <properties> - <help>File containing the secret key shared with remote end of tunnel</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> - </constraint> + <help>Secret key shared with remote end of tunnel</help> + <completionHelp> + <path>pki openvpn shared-secret</path> + </completionHelp> </properties> </leafNode> <node name="tls"> @@ -654,55 +650,30 @@ <help>Transport Layer Security (TLS) options</help> </properties> <children> - <leafNode name="auth-file"> - <properties> - <help>File containing tls static key for tls-auth</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> - </constraint> - </properties> - </leafNode> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - <leafNode name="crl-file"> + <leafNode name="auth-key"> <properties> - <help>File containing certificate revocation list (CRL) for this host</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> - </constraint> + <help>TLS shared secret key for tls-auth</help> + <completionHelp> + <path>pki openvpn shared-secret</path> + </completionHelp> </properties> </leafNode> - <leafNode name="dh-file"> + #include <include/pki/certificate.xml.i> + #include <include/pki/ca-certificate.xml.i> + <leafNode name="dh-params"> <properties> - <help>File containing Diffie Hellman parameters (server only)</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> - </constraint> + <help>Diffie Hellman parameters (server only)</help> + <completionHelp> + <path>pki dh</path> + </completionHelp> </properties> </leafNode> - #include <include/certificate-key.xml.i> - <leafNode name="crypt-file"> + <leafNode name="crypt-key"> <properties> - <help>File containing encryption key to authenticate control channel</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-exists" argument="--directory /config/auth"/> - </constraint> + <help>Static key to use to authenticate control channel</help> + <completionHelp> + <path>pki openvpn shared-secret</path> + </completionHelp> </properties> </leafNode> <leafNode name="tls-version-min"> diff --git a/interface-definitions/interfaces-tunnel.xml.in b/interface-definitions/interfaces-tunnel.xml.in index 56f8ea79c..b994bdafc 100644 --- a/interface-definitions/interfaces-tunnel.xml.in +++ b/interface-definitions/interfaces-tunnel.xml.in @@ -29,14 +29,7 @@ #include <include/interface/interface-ipv6-options.xml.i> #include <include/source-address-ipv4-ipv6.xml.i> #include <include/interface/tunnel-remote.xml.i> - <leafNode name="source-interface"> - <properties> - <help>Physical Interface used for underlaying traffic</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/source-interface.xml.i> <leafNode name="6rd-prefix"> <properties> <help>6rd network prefix</help> @@ -61,21 +54,7 @@ </constraint> </properties> </leafNode> - <leafNode name="dhcp-interface"> - <properties> - <help>dhcp interface</help> - <valueHelp> - <format>interface</format> - <description>DHCP interface that supplies the local IP address for this tunnel</description> - </valueHelp> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <constraint> - <regex>^(en|eth|br|bond|gnv|vxlan|wg|tun)[0-9]+$</regex> - </constraint> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="encapsulation"> <properties> <help>Encapsulation of this tunnel interface</help> diff --git a/interface-definitions/interfaces-vxlan.xml.in b/interface-definitions/interfaces-vxlan.xml.in index 7a286eaf2..56d01dfb6 100644 --- a/interface-definitions/interfaces-vxlan.xml.in +++ b/interface-definitions/interfaces-vxlan.xml.in @@ -92,6 +92,7 @@ #include <include/source-address-ipv4-ipv6.xml.i> #include <include/source-interface.xml.i> #include <include/interface/tunnel-remote.xml.i> + #include <include/interface/interface-vrf.xml.i> #include <include/vni.xml.i> </children> </tagNode> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 378251fed..773bde09c 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -42,12 +42,12 @@ </leafNode> <leafNode name="private-key"> <properties> - <help>Private key to use on that interface</help> - <completionHelp> - <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> - </completionHelp> + <help>Base64 encoded private key</help> + <constraint> + <regex>[0-9a-zA-Z\+/]{43}=$</regex> + </constraint> + <constraintErrorMessage>Key is not valid 44-character (32-bytes) base64</constraintErrorMessage> </properties> - <defaultValue>default</defaultValue> </leafNode> <tagNode name="peer"> <properties> @@ -59,7 +59,7 @@ </properties> <children> #include <include/generic-disable-node.xml.i> - <leafNode name="pubkey"> + <leafNode name="public-key"> <properties> <help>base64 encoded public key</help> <constraint> diff --git a/interface-definitions/snmp.xml.in b/interface-definitions/snmp.xml.in index f57103eac..2654449a1 100644 --- a/interface-definitions/snmp.xml.in +++ b/interface-definitions/snmp.xml.in @@ -129,6 +129,26 @@ <constraintErrorMessage>Location is limited to 255 characters or less</constraintErrorMessage> </properties> </leafNode> + <leafNode name="oid-enable"> + <properties> + <help>Enable specific oids</help> + <valueHelp> + <format>txt</format> + <description>Enable specific oids</description> + </valueHelp> + <valueHelp> + <format>route-table</format> + <description>Enable route table oids (ipCidrRouteTable inetCidrRouteTable)</description> + </valueHelp> + <completionHelp> + <list>route-table</list> + </completionHelp> + <constraint> + <regex>^(route-table)$</regex> + </constraint> + <constraintErrorMessage>Oid must be 'route-table'</constraintErrorMessage> + </properties> + </leafNode> <leafNode name="smux-peer"> <properties> <help>Register a subtree for SMUX-based processing</help> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 147f351f2..b28c86ae6 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -11,18 +11,6 @@ <priority>901</priority> </properties> <children> - <leafNode name="auto-update"> - <properties> - <help>Set auto-update interval for IPsec daemon</help> - <valueHelp> - <format>u32:30-65535</format> - <description>Auto-update interval (s)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 30-65535"/> - </constraint> - </properties> - </leafNode> <leafNode name="disable-uniqreqids"> <properties> <help>Option to disable requirement for unique IDs in the Security Database</help> @@ -52,6 +40,7 @@ <regex>^(disable|enable)$</regex> </constraint> </properties> + <defaultValue>disable</defaultValue> </leafNode> <leafNode name="lifetime"> <properties> @@ -394,7 +383,6 @@ </properties> <children> <leafNode name="dh-group"> - <defaultValue>2</defaultValue> <properties> <help>dh-grouphelp</help> <completionHelp> @@ -492,6 +480,7 @@ <regex>^(1|2|5|14|15|16|17|18|19|20|21|22|23|24|25|26|27|28|29|30|31|32)$</regex> </constraint> </properties> + <defaultValue>2</defaultValue> </leafNode> #include <include/vpn-ipsec-encryption.xml.i> #include <include/vpn-ipsec-hash.xml.i> @@ -509,22 +498,15 @@ <help>Sets to include an additional secrets file for strongSwan. Use an absolute path to specify the included file.</help> </properties> </leafNode> - <node name="ipsec-interfaces"> + <leafNode name="interface"> <properties> - <help>Interface to use for VPN [REQUIRED]</help> + <help>Onterface used for IPsec communication</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + <multi/> </properties> - <children> - <leafNode name="interface"> - <properties> - <help>IPsec interface [REQUIRED]</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - <multi/> - </properties> - </leafNode> - </children> - </node> + </leafNode> <node name="log"> <properties> <help>IPsec logging</help> @@ -648,37 +630,6 @@ <valueless/> </properties> </leafNode> - <node name="remote-access"> - <properties> - <help>remote-access global options</help> - </properties> - <children> - <node name="dhcp-pool"> - <properties> - <help>DHCP pool options for remote-access</help> - </properties> - <children> - <leafNode name="interface"> - <properties> - <help>Interface with DHCP server to use</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> - <leafNode name="server"> - <properties> - <help>DHCP server address</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of the DHCP server</description> - </valueHelp> - </properties> - </leafNode> - </children> - </node> - </children> - </node> </children> </node> <tagNode name="profile"> @@ -704,15 +655,7 @@ </valueHelp> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret key</help> - <valueHelp> - <format>txt</format> - <description>Pre-shared secret key</description> - </valueHelp> - </properties> - </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> </children> </node> <node name="bind"> @@ -739,102 +682,161 @@ #include <include/ipsec/ike-group.xml.i> </children> </tagNode> - <tagNode name="remote-access"> + <node name="remote-access"> <properties> - <help>Remote access IKEv2 VPN </help> + <help>IKEv2 remote access VPN</help> </properties> <children> - <node name="authentication"> + <tagNode name="connection"> <properties> - <help>Authentication for remote access</help> + <help>IKEv2 VPN connection name</help> </properties> <children> - #include <include/ipsec/authentication-id.xml.i> - #include <include/ipsec/authentication-x509.xml.i> - <leafNode name="client-mode"> + <node name="authentication"> <properties> - <help>Client authentication mode</help> - <completionHelp> - <list>eap-tls eap-mschapv2</list> - </completionHelp> - <valueHelp> - <format>eap-tls</format> - <description>EAP-TLS</description> - </valueHelp> + <help>Authentication for remote access</help> + </properties> + <children> + #include <include/ipsec/authentication-id.xml.i> + #include <include/ipsec/authentication-x509.xml.i> + <leafNode name="client-mode"> + <properties> + <help>Client authentication mode</help> + <completionHelp> + <list>eap-tls eap-mschapv2 eap-radius</list> + </completionHelp> + <valueHelp> + <format>eap-tls</format> + <description>Client uses EAP-TLS authentication</description> + </valueHelp> + <valueHelp> + <format>eap-mschapv2</format> + <description>Client uses EAP-MSCHAPv2 authentication</description> + </valueHelp> + <valueHelp> + <format>eap-radius</format> + <description>Client uses EAP-RADIUS authentication</description> + </valueHelp> + <constraint> + <regex>^(eap-tls|eap-mschapv2|eap-radius)$</regex> + </constraint> + </properties> + <defaultValue>eap-mschapv2</defaultValue> + </leafNode> + #include <include/auth-local-users.xml.i> + <leafNode name="server-mode"> + <properties> + <help>Server authentication mode</help> + <completionHelp> + <list>pre-shared-secret x509</list> + </completionHelp> + <valueHelp> + <format>pre-shared-secret</format> + <description>pre-shared-secret_description</description> + </valueHelp> + <valueHelp> + <format>x509</format> + <description>x509_description</description> + </valueHelp> + <constraint> + <regex>^(pre-shared-secret|x509)$</regex> + </constraint> + </properties> + <defaultValue>x509</defaultValue> + </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> + </children> + </node> + #include <include/generic-description.xml.i> + #include <include/generic-disable-node.xml.i> + #include <include/ipsec/esp-group.xml.i> + #include <include/ipsec/ike-group.xml.i> + #include <include/ipsec/local-address.xml.i> + #include <include/ipsec/local-traffic-selector.xml.i> + <leafNode name="timeout"> + <properties> + <help>Timeout to close connection if no data is transmitted</help> <valueHelp> - <format>eap-mschapv2</format> - <description>EAP-MSCHAPv2</description> + <format>u32:10-86400</format> + <description>Timeout in seconds (default 28800)</description> </valueHelp> <constraint> - <regex>^(eap-tls|eap-mschapv2)$</regex> + <validator name="numeric" argument="--range 10-86400"/> </constraint> </properties> - <defaultValue>eap-mschapv2</defaultValue> + <defaultValue>28800</defaultValue> </leafNode> - <node name="local-users"> + <leafNode name="pool"> <properties> - <help>Local user authentication for PPPoE server</help> + <help>Pool name used for IP address assignments</help> + <completionHelp> + <path>vpn ipsec remote-access pool</path> + <list>dhcp</list> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Pool name</description> + </valueHelp> + <multi/> </properties> - <children> - <tagNode name="username"> - <properties> - <help>User name for authentication</help> - </properties> - <children> - #include <include/generic-disable-node.xml.i> - <leafNode name="password"> - <properties> - <help>Password for authentication</help> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> - <leafNode name="server-mode"> + </leafNode> + <leafNode name="unique"> <properties> - <help>Server authentication mode</help> + <help>Connection uniqueness policy to enforce</help> <completionHelp> - <list>pre-shared-secret x509</list> + <list>never keep replace</list> </completionHelp> <valueHelp> - <format>pre-shared-secret</format> - <description>pre-shared-secret_description</description> + <format>never</format> + <description>Never enforce connection uniqueness policy</description> </valueHelp> <valueHelp> - <format>x509</format> - <description>x509_description</description> + <format>keep</format> + <description>Rejects new connection attempts if the same user already has an active connection</description> + </valueHelp> + <valueHelp> + <format>replace</format> + <description>Delete any existing connection if a new one for the same user gets established</description> </valueHelp> <constraint> - <regex>^(pre-shared-secret|x509)$</regex> + <regex>^(never|keep|replace)$</regex> </constraint> </properties> - <defaultValue>x509</defaultValue> </leafNode> - <leafNode name="pre-shared-secret"> + </children> + </tagNode> + <node name="dhcp"> + <properties> + <help>DHCP pool options for remote-access</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Interface with DHCP server to use</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py</script> + </completionHelp> + </properties> + </leafNode> + <leafNode name="server"> <properties> - <help>Pre-shared-secret used for server authentication</help> + <help>DHCP server address</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> </properties> </leafNode> </children> </node> - #include <include/generic-description.xml.i> - #include <include/generic-disable-node.xml.i> - #include <include/ipsec/esp-group.xml.i> - #include <include/ipsec/ike-group.xml.i> - #include <include/ipsec/local-address.xml.i> - #include <include/ipsec/local-traffic-selector.xml.i> - <node name="pool"> + <tagNode name="pool"> <properties> <help>IP address pool for remote-access users</help> </properties> <children> - <leafNode name="dhcp-enable"> - <properties> - <help>Enable DHCP pool for clients on this connection</help> - <valueless/> - </properties> - </leafNode> <leafNode name="exclude"> <properties> <help>Local IPv4 or IPv6 pool prefix exclusions</help> @@ -873,22 +875,20 @@ <!-- Include Accel-PPP definition here, maybe time for a rename? --> #include <include/accel-ppp/name-server.xml.i> </children> + </tagNode> + #include <include/radius-server-ipv4.xml.i> + <node name="radius"> + <children> + #include <include/radius-nas-identifier.xml.i> + <tagNode name="server"> + <children> + #include <include/accel-ppp/radius-additions-disable-accounting.xml.i> + </children> + </tagNode> + </children> </node> - <leafNode name="timeout"> - <properties> - <help>Timeout to close connection if no data is transmitted</help> - <valueHelp> - <format>u32:10-86400</format> - <description>Timeout in seconds (default 28800)</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 10-86400"/> - </constraint> - </properties> - <defaultValue>28800</defaultValue> - </leafNode> </children> - </tagNode> + </node> <node name="site-to-site"> <properties> <help>Site-to-site VPN</help> @@ -947,15 +947,7 @@ </constraint> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret key</help> - <valueHelp> - <format>txt</format> - <description>Pre-shared secret key</description> - </valueHelp> - </properties> - </leafNode> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> <leafNode name="remote-id"> <properties> <help>ID for remote authentication</help> @@ -1001,14 +993,7 @@ </properties> </leafNode> #include <include/generic-description.xml.i> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface to listen on</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py</script> - </completionHelp> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="force-encapsulation"> <properties> <help>Force UDP Encapsulation for ESP Payloads</help> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 4fbf3fa44..cf31af70f 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -70,51 +70,8 @@ </completionHelp> </properties> </leafNode> - <leafNode name="pre-shared-secret"> - <properties> - <help>Pre-shared secret for IPsec</help> - </properties> - </leafNode> - <node name="x509"> - <properties> - <help>X.509 certificate</help> - </properties> - <children> - #include <include/certificate-ca.xml.i> - <leafNode name="crl-file"> - <properties> - <help>File containing the X.509 Certificate Revocation List (CRL)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-cert-file"> - <properties> - <help>File containing the X.509 certificate for the remote access VPN server (this host)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-key-file"> - <properties> - <help>File containing the private key for the X.509 certificate for the remote access VPN server (this host)</help> - <valueHelp> - <format>txt</format> - <description>File in /config/auth</description> - </valueHelp> - </properties> - </leafNode> - <leafNode name="server-key-password"> - <properties> - <help>Password that protects the private key</help> - </properties> - </leafNode> - </children> - </node> + #include <include/ipsec/authentication-pre-shared-secret.xml.i> + #include <include/ipsec/authentication-x509.xml.i> </children> </node> <leafNode name="ike-lifetime"> @@ -128,6 +85,7 @@ <validator name="numeric" argument="--range 30-86400"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> <leafNode name="lifetime"> <properties> @@ -140,7 +98,10 @@ <validator name="numeric" argument="--range 30-86400"/> </constraint> </properties> + <defaultValue>3600</defaultValue> </leafNode> + #include <include/ipsec/esp-group.xml.i> + #include <include/ipsec/ike-group.xml.i> </children> </node> #include <include/accel-ppp/wins-server.xml.i> @@ -159,11 +120,7 @@ <help>Description for L2TP remote-access settings</help> </properties> </leafNode> - <leafNode name="dhcp-interface"> - <properties> - <help>DHCP interface to listen on</help> - </properties> - </leafNode> + #include <include/dhcp-interface.xml.i> <leafNode name="idle"> <properties> <help>PPP idle timeout</help> @@ -248,11 +205,7 @@ <help>Maximum number of tries to send Access-Request/Accounting-Request queries</help> </properties> </leafNode> - <leafNode name="nas-identifier"> - <properties> - <help>Value to send to RADIUS server in NAS-Identifier attribute and to be matched in DM/CoA requests.</help> - </properties> - </leafNode> + #include <include/radius-nas-identifier.xml.i> <node name="dae-server"> <properties> <help>IPv4 address and port to bind Dynamic Authorization Extension server (DM/CoA)</help> diff --git a/interface-definitions/vpn_openconnect.xml.in b/interface-definitions/vpn_openconnect.xml.in index 1a9d39a12..a33ff67ea 100644 --- a/interface-definitions/vpn_openconnect.xml.in +++ b/interface-definitions/vpn_openconnect.xml.in @@ -32,26 +32,7 @@ </completionHelp> </properties> </leafNode> - <node name="local-users"> - <properties> - <help>Local user authentication for SSL VPN server</help> - </properties> - <children> - <tagNode name="username"> - <properties> - <help>User name for authentication</help> - </properties> - <children> - #include <include/generic-disable-node.xml.i> - <leafNode name="password"> - <properties> - <help>Password for authentication</help> - </properties> - </leafNode> - </children> - </tagNode> - </children> - </node> + #include <include/auth-local-users.xml.i> #include <include/radius-server-ipv4.xml.i> <node name="radius"> <children> @@ -75,7 +56,7 @@ </node> <node name="listen-ports"> <properties> - <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + <help>Specify custom ports to use for client connections</help> </properties> <children> <leafNode name="tcp"> @@ -108,12 +89,11 @@ </node> <node name="ssl"> <properties> - <help>SSL Certificate, SSL Key and CA (/config/auth)</help> + <help>SSL Certificate, SSL Key and CA</help> </properties> <children> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - #include <include/certificate-key.xml.i> + #include <include/pki/ca-certificate.xml.i> + #include <include/pki/certificate-key.xml.i> </children> </node> <node name="network-settings"> diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index c09603028..3576bac90 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -50,12 +50,11 @@ </node> <node name="ssl"> <properties> - <help>SSL Certificate, SSL Key and CA (/config/user-data/sstp)</help> + <help>SSL Certificate, SSL Key and CA</help> </properties> <children> - #include <include/certificate.xml.i> - #include <include/certificate-ca.xml.i> - #include <include/certificate-key.xml.i> + #include <include/pki/ca-certificate.xml.i> + #include <include/pki/certificate.xml.i> </children> </node> </children> diff --git a/interface-definitions/vrf.xml.in b/interface-definitions/vrf.xml.in index 426884a11..9d513945c 100644 --- a/interface-definitions/vrf.xml.in +++ b/interface-definitions/vrf.xml.in @@ -19,7 +19,7 @@ <constraint> <validator name="vrf-name"/> </constraint> - <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\n</constraintErrorMessage> + <constraintErrorMessage>VRF instance name must be 15 characters or less and can not\nbe named as regular network interfaces.\nA name must starts from a letter.\n</constraintErrorMessage> <valueHelp> <format>txt</format> <description>VRF instance name</description> @@ -76,13 +76,13 @@ <properties> <help>Routing table associated with this instance</help> <valueHelp> - <format>100-2147483647</format> + <format>100-65535</format> <description>Routing table ID</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 100-2147483647"/> + <validator name="numeric" argument="--range 100-65535"/> </constraint> - <constraintErrorMessage>VRF routing table must be in range from 100 to 2147483647</constraintErrorMessage> + <constraintErrorMessage>VRF routing table must be in range from 100 to 65535</constraintErrorMessage> </properties> </leafNode> #include <include/vni.xml.i> diff --git a/op-mode-definitions/generate-ipsec-profile.xml.in b/op-mode-definitions/generate-ipsec-profile.xml.in new file mode 100644 index 000000000..be9227971 --- /dev/null +++ b/op-mode-definitions/generate-ipsec-profile.xml.in @@ -0,0 +1,145 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="ipsec"> + <properties> + <help>Generate IPsec related configurations</help> + </properties> + <children> + <node name="profile"> + <properties> + <help>Generate IKEv2 IPSec remote-access VPN profiles</help> + </properties> + <children> + <tagNode name="ios-remote-access"> + <properties> + <help>Generate iOS profile for specified remote-access connection name</help> + <completionHelp> + <path>vpn ipsec remote-access connection</path> + </completionHelp> + </properties> + <children> + <tagNode name="remote"> + <properties> + <help>Remote address where the client will connect to</help> + <completionHelp> + <list><fqdn></list> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7"</command> + <children> + <tagNode name="name"> + <properties> + <help>Connection name as seen in the VPN application</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --name "$9"</command> + <children> + <tagNode name="profile"> + <properties> + <help>Profile name as seen under system profiles</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --name "$9" --profile "${11}"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="profile"> + <properties> + <help>Profile name as seen under system profiles</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --profile "$9"</command> + <children> + <tagNode name="name"> + <properties> + <help>Connection name as seen in the VPN application</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os ios --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <tagNode name="windows-remote-access"> + <properties> + <help>Generate iOS profile for specified remote-access connection name</help> + <completionHelp> + <path>vpn ipsec remote-access connection</path> + </completionHelp> + </properties> + <children> + <tagNode name="remote"> + <properties> + <help>Remote address where the client will connect to</help> + <completionHelp> + <list><fqdn></list> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7"</command> + <children> + <tagNode name="name"> + <properties> + <help>Connection name as seen in the VPN application</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --name "$9"</command> + <children> + <tagNode name="profile"> + <properties> + <help>Profile name as seen under system profiles</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --name "$9" --profile "${11}"</command> + </tagNode> + </children> + </tagNode> + <tagNode name="profile"> + <properties> + <help>Profile name as seen under system profiles</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --profile "$9"</command> + <children> + <tagNode name="name"> + <properties> + <help>Connection name as seen in the VPN application</help> + <completionHelp> + <list><name></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/ikev2_profile_generator.py --os windows --connection "$5" --remote "$7" --profile "$9" --name "${11}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/generate-wireguard.xml.in b/op-mode-definitions/generate-wireguard.xml.in new file mode 100644 index 000000000..6557b463b --- /dev/null +++ b/op-mode-definitions/generate-wireguard.xml.in @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="generate"> + <children> + <node name="wireguard"> + <properties> + <help>Generate Wireguard keys</help> + </properties> + <children> + <tagNode name="client-config"> + <properties> + <help>Generate Client config QR code</help> + <completionHelp> + <list><client-name></list> + </completionHelp> + </properties> + <children> + <tagNode name="interface"> + <properties> + <help>Local interface used for connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + </completionHelp> + </properties> + <children> + <tagNode name="server"> + <properties> + <help>IP address/FQDN used for client connection</help> + <completionHelp> + <script>${vyos_completion_dir}/list_local_ips.sh --both</script> + <list><hostname></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv4/IPv6 address used by client</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}"</command> + <children> + <tagNode name="address"> + <properties> + <help>IPv4/IPv6 address used by client</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}" --address "${12}"</command> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + </children> + </tagNode> + <leafNode name="key-pair"> + <properties> + <help>Generate Wireguard key pair for use with server or peer</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "noname"</command> + </leafNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/include/bgp/afi-common.xml.i b/op-mode-definitions/include/bgp/afi-common.xml.i index e48482282..7fc59f3b0 100644 --- a/op-mode-definitions/include/bgp/afi-common.xml.i +++ b/op-mode-definitions/include/bgp/afi-common.xml.i @@ -31,10 +31,25 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </leafNode> -<leafNode name="summary"> +<node name="summary"> <properties> <help>Summary of BGP neighbor status</help> </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> -</leafNode> + <children> + <leafNode name="established"> + <properties> + <help>Show only sessions in Established state</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="failed"> + <properties> + <help>Show only sessions not in Established state</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> +</node> +#include <include/vtysh-generic-wide.xml.i> <!-- included end --> diff --git a/op-mode-definitions/include/vtysh-generic-wide.xml.i b/op-mode-definitions/include/vtysh-generic-wide.xml.i new file mode 100644 index 000000000..acc68b4c0 --- /dev/null +++ b/op-mode-definitions/include/vtysh-generic-wide.xml.i @@ -0,0 +1,8 @@ +<!-- included start from vtysh-generic-wide.xml.i --> +<leafNode name="wide"> + <properties> + <help>Increase table width for longer prefixes</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> +</leafNode> +<!-- included end --> diff --git a/op-mode-definitions/monitor-bridge.xml.in b/op-mode-definitions/monitor-bridge.xml.in new file mode 100644 index 000000000..712a924f1 --- /dev/null +++ b/op-mode-definitions/monitor-bridge.xml.in @@ -0,0 +1,33 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="monitor"> + <children> + <node name="bridge"> + <properties> + <help>Monitoring bridge database generated objects and address changes</help> + </properties> + <command>sudo bridge monitor all</command> + <children> + <node name="link"> + <command>sudo bridge monitor link</command> + <properties> + <help>Monitoring bridge database generated connection interface changes</help> + </properties> + </node> + <node name="fdb"> + <command>sudo bridge monitor fdb</command> + <properties> + <help>Monitor the forwarding database changes generated by the bridge database</help> + </properties> + </node> + <node name="mdb"> + <command>sudo bridge monitor mdb</command> + <properties> + <help>Monitor the multicast database changes generated by the bridge database</help> + </properties> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/openvpn.xml.in b/op-mode-definitions/openvpn.xml.in index f8dc0cff0..781fbdc9d 100644 --- a/op-mode-definitions/openvpn.xml.in +++ b/op-mode-definitions/openvpn.xml.in @@ -1,49 +1,5 @@ <?xml version="1.0"?> <interfaceDefinition> - <node name="generate"> - <children> - <node name="openvpn"> - <properties> - <help>OpenVPN key generation tool</help> - </properties> - <children> - <tagNode name="key"> - <properties> - <help>Generate shared-secret key with specified file name</help> - <completionHelp> - <list><filename></list> - </completionHelp> - </properties> - <command> - result=1; - key_path=$4 - full_path= - - if echo $key_path | egrep -ve '^/.*' > /dev/null; then - full_path=/config/auth/$key_path - else - full_path=$key_path - fi - - key_dir=`dirname $full_path` - if [ ! -d $key_dir ]; then - echo "Directory $key_dir does not exist!" - exit 1 - fi - - echo "Generating OpenVPN key to $full_path" - sudo /usr/sbin/openvpn --genkey secret "$full_path" - result=$? - if [ $result = 0 ]; then - echo "Your new local OpenVPN key has been generated" - fi - /usr/libexec/vyos/validators/file-exists --directory /config/auth "$full_path" - </command> - </tagNode> - </children> - </node> - </children> - </node> <node name="reset"> <properties> <help>Reset a service</help> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index 9c6b56a68..a11814c8a 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -20,9 +20,18 @@ </completionHelp> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated CA certificate into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$7" --sign "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> - <help>Commands for installing generated certificate into running configuration</help> + <help>Commands for installing generated CA certificate into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> @@ -32,9 +41,18 @@ </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "noname" --sign "$5"</command> </tagNode> + <tagNode name="file"> + <properties> + <help>Write generated CA certificate into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ca "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> - <help>Commands for installing generated certificate into running configuration</help> + <help>Commands for installing generated CA certificate into running configuration</help> <completionHelp> <list><CA name></list> </completionHelp> @@ -54,6 +72,15 @@ <help>Generate self-signed certificate</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated self-signed certificate into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$6" --self-sign --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated self-signed certificate into running configuration</help> @@ -74,9 +101,18 @@ </completionHelp> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated signed certificate into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$7" --sign "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> - <help>Commands for installing generated certificate into running configuration</help> + <help>Commands for installing generated signed certificate into running configuration</help> <completionHelp> <list><certificate name></list> </completionHelp> @@ -86,6 +122,15 @@ </children> <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "noname" --sign "$5"</command> </tagNode> + <tagNode name="file"> + <properties> + <help>Write generated certificate request and key into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --certificate "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated certificate private key into running configuration</help> @@ -106,6 +151,15 @@ </completionHelp> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated CRL into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --crl "$4" --file</command> + </tagNode> <leafNode name="install"> <properties> <help>Commands for installing generated CRL into running configuration</help> @@ -120,6 +174,15 @@ <help>Generate DH parameters</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated DH parameters into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --dh "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated DH parameters into running configuration</help> @@ -137,6 +200,15 @@ <help>Generate a key pair</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated key pair into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --keypair "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated key pair into running configuration</help> @@ -159,6 +231,15 @@ <help>Generate OpenVPN shared secret key</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated OpenVPN shared secret key into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --openvpn "$6" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated OpenVPN shared secret key into running configuration</help> @@ -178,6 +259,15 @@ <help>Generate SSH key</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated SSH keys into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --ssh "$5" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated SSH key into running configuration</help> @@ -200,6 +290,15 @@ <help>Generate Wireguard key pair for use with server or peer</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated Wireguard keys into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --key "$6" --file</command> + </tagNode> <tagNode name="install"> <properties> <help>Commands for installing generated Wireguard key into running configuration</help> @@ -217,9 +316,18 @@ <help>Generate pre-shared key for use with a Wireguard peer</help> </properties> <children> + <tagNode name="file"> + <properties> + <help>Write generated Wireguard PSK into the specified filename</help> + <completionHelp> + <list><filename></list> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/pki.py --action generate --wireguard --psk "$6" --file</command> + </tagNode> <tagNode name="install"> <properties> - <help>Commands for installing generated Wireguard psk on specified peer into running configuration</help> + <help>Commands for installing generated Wireguard PSK on specified peer into running configuration</help> <completionHelp> <list><peer></list> </completionHelp> diff --git a/op-mode-definitions/show-bgp.xml.in b/op-mode-definitions/show-bgp.xml.in index 36e7062df..c33a9dacf 100644 --- a/op-mode-definitions/show-bgp.xml.in +++ b/op-mode-definitions/show-bgp.xml.in @@ -8,7 +8,81 @@ </properties> <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> + <node name="cidr-only"> + <properties> + <help>Display only routes with non-natural netmasks</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/vtysh-generic-wide.xml.i> + </children> + </node> #include <include/bgp/show-bgp-common.xml.i> + <node name="mac"> + <properties> + <help>MAC address</help> + </properties> + <children> + <leafNode name="hash"> + <properties> + <help>MAC address database</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> + <node name="martian"> + <properties> + <help>martian next-hops</help> + </properties> + <children> + <leafNode name="next-hop"> + <properties> + <help>martian next-hop database</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + </children> + </node> + <leafNode name="memory"> + <properties> + <help>Global BGP memory statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <node name="nexthop"> + <properties> + <help>Show BGP nexthop table</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/vtysh-generic-detail.xml.i> + </children> + </node> + <tagNode name="nexthop"> + <properties> + <help>IPv4/IPv6 nexthop address</help> + <completionHelp> + <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + #include <include/vtysh-generic-detail.xml.i> + </children> + </tagNode> + <leafNode name="statistics"> + <properties> + <help>BGP RIB advertisement statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> + <leafNode name="statistics-all"> + <properties> + <help>Display number of prefixes for all afi/safi</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </leafNode> <leafNode name="vrf"> <properties> <help>Show BGP VRF information</help> @@ -28,6 +102,7 @@ #include <include/bgp/show-bgp-common.xml.i> </children> </tagNode> + #include <include/vtysh-generic-wide.xml.i> </children> </node> </children> diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in index 78c350e44..0f8d3064d 100644 --- a/op-mode-definitions/show-bridge.xml.in +++ b/op-mode-definitions/show-bridge.xml.in @@ -2,11 +2,24 @@ <interfaceDefinition> <node name="show"> <children> + <node name="bridge"> + <properties> + <help>Show bridging information</help> + </properties> + <children> + <leafNode name="vlan"> + <properties> + <help>View the VLAN filter settings of the bridge</help> + </properties> + <command>bridge -c vlan show</command> + </leafNode> + </children> + </node> <leafNode name="bridge"> <properties> <help>Show bridging information</help> </properties> - <command>/sbin/brctl show</command> + <command>bridge -c link show</command> </leafNode> <tagNode name="bridge"> <properties> @@ -15,25 +28,19 @@ <script>${vyos_completion_dir}/list_interfaces.py --type bridge</script> </completionHelp> </properties> - <command>/sbin/brctl show $3</command> + <command>bridge -c link show | grep "master $3"</command> <children> - <leafNode name="macs"> - <properties> - <help>Show bridge Media Access Control (MAC) address table</help> - </properties> - <command>/sbin/brctl showmacs $3</command> - </leafNode> - <leafNode name="spanning-tree"> + <leafNode name="mdb"> <properties> - <help>Show bridge spanning tree information</help> + <help>Displays the multicast group database for the bridge</help> </properties> - <command>/sbin/brctl showstp $3</command> + <command>bridge -c mdb show dev $3</command> </leafNode> <leafNode name="fdb"> <properties> <help>Show the forwarding database of the bridge</help> </properties> - <command>/usr/sbin/bridge -c fdb show br $3</command> + <command>bridge -c fdb show br $3</command> </leafNode> </children> </tagNode> diff --git a/op-mode-definitions/show-interfaces-bridge.xml.in b/op-mode-definitions/show-interfaces-bridge.xml.in index cc4b248b6..85fde95b5 100644 --- a/op-mode-definitions/show-interfaces-bridge.xml.in +++ b/op-mode-definitions/show-interfaces-bridge.xml.in @@ -33,12 +33,6 @@ </properties> <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=bridge --action=show</command> </leafNode> - <leafNode name="vlan"> - <properties> - <help>View the VLAN filter settings of the bridge</help> - </properties> - <command>/usr/sbin/bridge -c vlan show</command> - </leafNode> </children> </node> </children> diff --git a/op-mode-definitions/show-interfaces-wireguard.xml.in b/op-mode-definitions/show-interfaces-wireguard.xml.in new file mode 100644 index 000000000..863357ef7 --- /dev/null +++ b/op-mode-definitions/show-interfaces-wireguard.xml.in @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="interfaces"> + <children> + <tagNode name="wireguard"> + <properties> + <help>Show Wireguard interface information</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf="$4"</command> + <children> + <leafNode name="allowed-ips"> + <properties> + <help>Show all IP addresses allowed for the specified interface</help> + </properties> + <command>sudo wg show "$4" allowed-ips</command> + </leafNode> + <leafNode name="endpoints"> + <properties> + <help>Show all endpoints for the specified interface</help> + </properties> + <command>sudo wg show "$4" endpoints</command> + </leafNode> + <leafNode name="peers"> + <properties> + <help>Show all peer IDs for the specified interface</help> + </properties> + <command>sudo wg show "$4" peers</command> + </leafNode> + <leafNode name="public-key"> + <properties> + <help>Show interface public-key</help> + </properties> + <command>sudo wg show "$4" public-key</command> + </leafNode> + <leafNode name="summary"> + <properties> + <help>Shows current configuration and device information</help> + </properties> + <command>sudo wg show "$4"</command> + </leafNode> + </children> + </tagNode> + <node name="wireguard"> + <properties> + <help>Show Wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> + <children> + <leafNode name="detail"> + <properties> + <help>Show detailed Wireguard interface information</help> + </properties> + <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> + </leafNode> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/wireguard.xml.in b/op-mode-definitions/wireguard.xml.in deleted file mode 100644 index 0df838b50..000000000 --- a/op-mode-definitions/wireguard.xml.in +++ /dev/null @@ -1,189 +0,0 @@ -<?xml version="1.0"?> -<!-- Wireguard key management --> -<interfaceDefinition> - <node name="generate"> - <children> - <node name="wireguard"> - <properties> - <help>Generate Wireguard keys</help> - </properties> - <children> - <leafNode name="default-keypair"> - <properties> - <help>Generate the default Wireguard keypair</help> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey</command> - </leafNode> - <leafNode name="preshared-key"> - <properties> - <help>Generate a Wireguard preshared key</help> - </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --genpsk</command> - </leafNode> - <tagNode name="named-keypairs"> - <properties> - <help>Generate specified Wireguard keypairs</help> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard.py --genkey --location "$4"</command> - </tagNode> - <tagNode name="client-config"> - <properties> - <help>Generate Client config QR code</help> - <completionHelp> - <list><client-name></list> - </completionHelp> - </properties> - <children> - <tagNode name="interface"> - <properties> - <help>Local interface used for connection</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> - </completionHelp> - </properties> - <children> - <tagNode name="server"> - <properties> - <help>IP address/FQDN used for client connection</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - <list><hostname></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8"</command> - <children> - <tagNode name="address"> - <properties> - <help>IPv4/IPv6 address used by client</help> - <completionHelp> - <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}"</command> - <children> - <tagNode name="address"> - <properties> - <help>IPv4/IPv6 address used by client</help> - <completionHelp> - <list><x.x.x.x> <h:h:h:h:h:h:h:h></list> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard_client.py --name "$4" --interface "$6" --server "$8" --address "${10}" --address "${12}"</command> - </tagNode> - </children> - </tagNode> - </children> - </tagNode> - </children> - </tagNode> - </children> - </tagNode> - </children> - </node> - </children> - </node> - <node name="show"> - <children> - <node name="wireguard"> - <properties> - <help>Show Wireguard properties</help> - </properties> - <children> - <node name="keypairs"> - <properties> - <help>Show Wireguard keys</help> - </properties> - <children> - <tagNode name="pubkey"> - <properties> - <help>Show specified Wireguard public key</help> - <completionHelp> - <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> - </completionHelp> - </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --showpub --location "$5"</command> - </tagNode> - <tagNode name="privkey"> - <properties> - <help>Show specified Wireguard private key</help> - <completionHelp> - <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> - </completionHelp> - </properties> - <command>${vyos_op_scripts_dir}/wireguard.py --showpriv --location "$5"</command> - </tagNode> - </children> - </node> - </children> - </node> - <node name="interfaces"> - <children> - <tagNode name="wireguard"> - <properties> - <help>Show Wireguard interface information</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces.py --type wireguard</script> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard.py --showinterface "$4"</command> - <children> - <leafNode name="allowed-ips"> - <properties> - <help>Show all IP addresses allowed for the specified interface</help> - </properties> - <command>sudo wg show "$4" allowed-ips</command> - </leafNode> - <leafNode name="endpoints"> - <properties> - <help>Show all endpoints for the specified interface</help> - </properties> - <command>sudo wg show "$4" endpoints</command> - </leafNode> - <leafNode name="peers"> - <properties> - <help>Show all peer IDs for the specified interface</help> - </properties> - <command>sudo wg show "$4" peers</command> - </leafNode> - <!-- more commands upon request --> - </children> - </tagNode> - <node name="wireguard"> - <properties> - <help>Show Wireguard interface information</help> - </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show-brief</command> - <children> - <leafNode name="detail"> - <properties> - <help>Show detailed Wireguard interface information</help> - </properties> - <command>${vyos_op_scripts_dir}/show_interfaces.py --intf-type=wireguard --action=show</command> - </leafNode> - </children> - </node> - </children> - </node> - </children> - </node> - <node name="delete"> - <children> - <node name="wireguard"> - <properties> - <help>Delete Wireguard properties</help> - </properties> - <children> - <tagNode name="keypair"> - <properties> - <help>Delete a Wireguard keypair</help> - <completionHelp> - <script>${vyos_op_scripts_dir}/wireguard.py --listkdir</script> - </completionHelp> - </properties> - <command>sudo ${vyos_op_scripts_dir}/wireguard.py --delkdir --location "$4"</command> - </tagNode> - </children> - </node> - </children> - </node> -</interfaceDefinition> diff --git a/python/vyos/airbag.py b/python/vyos/airbag.py index 510ab7f46..a20f44207 100644 --- a/python/vyos/airbag.py +++ b/python/vyos/airbag.py @@ -18,7 +18,6 @@ from datetime import datetime from vyos import debug from vyos.logger import syslog -from vyos.version import get_version from vyos.version import get_full_version_data @@ -78,7 +77,7 @@ def bug_report(dtype, value, trace): information.update({ 'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'), 'trace': trace, - 'instructions': COMMUNITY if 'rolling' in get_version() else SUPPORTED, + 'instructions': INSTRUCTIONS, 'note': note, }) @@ -162,20 +161,13 @@ When reporting problems, please include as much information as possible: """ -COMMUNITY = """\ -- Make sure you are running the latest version of the code available at - https://downloads.vyos.io/rolling/current/amd64/vyos-rolling-latest.iso -- Consult the forum to see how to handle this issue - https://forum.vyos.io -- Join our community on slack where our users exchange help and advice - https://vyos.slack.com -""".strip() - -SUPPORTED = """\ -- Make sure you are running the latest stable version of VyOS - the code is available at https://downloads.vyos.io/?dir=release/current -- Contact us using the online help desk +INSTRUCTIONS = """\ +- Contact us using the online help desk if you have a subscription: https://support.vyos.io/ -- Join our community on slack where our users exchange help and advice +- Make sure you are running the latest version of VyOS available at: + https://vyos.net/get/ +- Consult the community forum to see how to handle this issue: + https://forum.vyos.io +- Join us on Slack where our users exchange help and advice: https://vyos.slack.com """.strip() diff --git a/python/vyos/configquery.py b/python/vyos/configquery.py index ed7346f1f..1cdcbcf39 100644 --- a/python/vyos/configquery.py +++ b/python/vyos/configquery.py @@ -18,9 +18,16 @@ A small library that allows querying existence or value(s) of config settings from op mode, and execution of arbitrary op mode commands. ''' +import re +import json +from copy import deepcopy from subprocess import STDOUT -from vyos.util import popen +import vyos.util +import vyos.xml +from vyos.config import Config +from vyos.configtree import ConfigTree +from vyos.configsource import ConfigSourceSession class ConfigQueryError(Exception): pass @@ -51,32 +58,59 @@ class CliShellApiConfigQuery(GenericConfigQuery): def exists(self, path: list): cmd = ' '.join(path) - (_, err) = popen(f'cli-shell-api existsActive {cmd}') + (_, err) = vyos.util.popen(f'cli-shell-api existsActive {cmd}') if err: return False return True def value(self, path: list): cmd = ' '.join(path) - (out, err) = popen(f'cli-shell-api returnActiveValue {cmd}') + (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValue {cmd}') if err: raise ConfigQueryError('No value for given path') return out def values(self, path: list): cmd = ' '.join(path) - (out, err) = popen(f'cli-shell-api returnActiveValues {cmd}') + (out, err) = vyos.util.popen(f'cli-shell-api returnActiveValues {cmd}') if err: raise ConfigQueryError('No values for given path') return out +class ConfigTreeQuery(GenericConfigQuery): + def __init__(self): + super().__init__() + + config_source = ConfigSourceSession() + self.configtree = Config(config_source=config_source) + + def exists(self, path: list): + return self.configtree.exists(path) + + def value(self, path: list): + return self.configtree.return_value(path) + + def values(self, path: list): + return self.configtree.return_values(path) + + def list_nodes(self, path: list): + return self.configtree.list_nodes(path) + + def get_config_dict(self, path=[], effective=False, key_mangling=None, + get_first_key=False, no_multi_convert=False, + no_tag_node_value_mangle=False): + return self.configtree.get_config_dict(path, effective=effective, + key_mangling=key_mangling, get_first_key=get_first_key, + no_multi_convert=no_multi_convert, + no_tag_node_value_mangle=no_tag_node_value_mangle) + class VbashOpRun(GenericOpRun): def __init__(self): super().__init__() def run(self, path: list, **kwargs): cmd = ' '.join(path) - (out, err) = popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs) + (out, err) = vyos.util.popen(f'. /opt/vyatta/share/vyatta-op/functions/interpreter/vyatta-op-run; _vyatta_op_run {cmd}', stderr=STDOUT, **kwargs) if err: raise ConfigQueryError(out) return out diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 670e6c7fc..f28ad09c5 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -10,14 +10,14 @@ # See the GNU Lesser General Public License for more details. # # You should have received a copy of the GNU Lesser General Public License along with this library; -# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA import os import re import sys import subprocess -from vyos.util import call +from vyos.util import is_systemd_service_running CLI_SHELL_API = '/bin/cli-shell-api' SET = '/opt/vyatta/sbin/my_set' @@ -73,8 +73,7 @@ def inject_vyos_env(env): env['vyos_validators_dir'] = '/usr/libexec/vyos/validators' # if running the vyos-configd daemon, inject the vyshim env var - ret = call('systemctl is-active --quiet vyos-configd.service') - if not ret: + if is_systemd_service_running('vyos-configd.service'): env['vyshim'] = '/usr/sbin/vyshim' return env diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 979e28b11..4279e6982 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -149,9 +149,38 @@ def verify_eapol(config): recurring validation of EAPoL configuration. """ if 'eapol' in config: - if not {'cert_file', 'key_file'} <= set(config['eapol']): - raise ConfigError('Both cert and key-file must be specified '\ - 'when using EAPoL!') + if 'certificate' not in config['eapol']: + raise ConfigError('Certificate must be specified when using EAPoL!') + + if 'certificate' not in config['pki']: + raise ConfigError('Invalid certificate specified for EAPoL') + + cert_name = config['eapol']['certificate'] + + if cert_name not in config['pki']['certificate']: + raise ConfigError('Invalid certificate specified for EAPoL') + + cert = config['pki']['certificate'][cert_name] + + if 'certificate' not in cert or 'private' not in cert or 'key' not in cert['private']: + raise ConfigError('Invalid certificate/private key specified for EAPoL') + + if 'password_protected' in cert['private']: + raise ConfigError('Encrypted private key cannot be used for EAPoL') + + if 'ca_certificate' in config['eapol']: + if 'ca' not in config['pki']: + raise ConfigError('Invalid CA certificate specified for EAPoL') + + ca_cert_name = config['eapol']['ca_certificate'] + + if ca_cert_name not in config['pki']['ca']: + raise ConfigError('Invalid CA certificate specified for EAPoL') + + ca_cert = config['pki']['ca'][cert_name] + + if 'certificate' not in ca_cert: + raise ConfigError('Invalid CA certificate specified for EAPoL') def verify_mirror(config): """ @@ -315,7 +344,7 @@ def verify_accel_ppp_base_service(config): # vertify auth settings if dict_search('authentication.mode', config) == 'local': if not dict_search('authentication.local_users', config): - raise ConfigError('PPPoE local auth mode requires local users to be configured!') + raise ConfigError('Authentication mode local requires local users to be configured!') for user in dict_search('authentication.local_users.username', config): user_config = config['authentication']['local_users']['username'][user] @@ -339,7 +368,7 @@ def verify_accel_ppp_base_service(config): raise ConfigError(f'Missing RADIUS secret key for server "{server}"') if 'gateway_address' not in config: - raise ConfigError('PPPoE server requires gateway-address to be configured!') + raise ConfigError('Server requires gateway-address to be configured!') if 'name_server_ipv4' in config: if len(config['name_server_ipv4']) > 2: diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 9921e3b5f..03006c383 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -22,7 +22,10 @@ directories = { "migrate": "/opt/vyatta/etc/config-migrate/migrate", "log": "/var/log/vyatta", "templates": "/usr/share/vyos/templates/", - "certbot": "/config/auth/letsencrypt" + "certbot": "/config/auth/letsencrypt", + "api_schema": "/usr/libexec/vyos/services/api/graphql/graphql/schema/", + "api_templates": "/usr/libexec/vyos/services/api/graphql/recipes/templates/" + } cfg_group = 'vyattacfg' diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index 08b7af90b..a1928ba51 100644..100755 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -311,6 +311,28 @@ class Interface(Control): cmd = 'ip link del dev {ifname}'.format(**self.config) return self._cmd(cmd) + def _set_vrf_ct_zone(self, vrf): + """ + Add/Remove rules in nftables to associate traffic in VRF to an + individual conntack zone + """ + if vrf: + # Get routing table ID for VRF + vrf_table_id = get_interface_config(vrf).get('linkinfo', {}).get( + 'info_data', {}).get('table') + # Add map element with interface and zone ID + if vrf_table_id: + self._cmd( + f'nft add element inet vrf_zones ct_iface_map {{ "{self.ifname}" : {vrf_table_id} }}' + ) + else: + nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{self.ifname}" }}' + # Check if deleting is possible first to avoid raising errors + _, err = self._popen(f'nft -c {nft_del_element}') + if not err: + # Remove map element + self._cmd(f'nft {nft_del_element}') + def get_min_mtu(self): """ Get hardware minimum supported MTU @@ -401,6 +423,7 @@ class Interface(Control): >>> Interface('eth0').set_vrf() """ self.set_interface('vrf', vrf) + self._set_vrf_ct_zone(vrf) def set_arp_cache_tmo(self, tmo): """ diff --git a/python/vyos/ifconfig/l2tpv3.py b/python/vyos/ifconfig/l2tpv3.py index 7ff0fdd0e..fcd1fbf81 100644 --- a/python/vyos/ifconfig/l2tpv3.py +++ b/python/vyos/ifconfig/l2tpv3.py @@ -13,8 +13,28 @@ # You should have received a copy of the GNU Lesser General Public # License along with this library. If not, see <http://www.gnu.org/licenses/>. +from time import sleep +from time import time +from vyos.util import run from vyos.ifconfig.interface import Interface +def wait_for_add_l2tpv3(timeout=10, sleep_interval=1, cmd=None): + ''' + In some cases, we need to wait until local address is assigned. + And only then can the l2tpv3 tunnel be configured. + For example when ipv6 address in tentative state + or we wait for some routing daemon for remote address. + ''' + start_time = time() + test_command = cmd + while True: + if (start_time + timeout) < time(): + return None + result = run(test_command) + if result == 0: + return True + sleep(sleep_interval) + @Interface.register class L2TPv3If(Interface): """ @@ -43,7 +63,9 @@ class L2TPv3If(Interface): cmd += ' encap {encapsulation}' cmd += ' local {source_address}' cmd += ' remote {remote}' - self._cmd(cmd.format(**self.config)) + c = cmd.format(**self.config) + # wait until the local/remote address is available, but no more 10 sec. + wait_for_add_l2tpv3(cmd=c) # setup session cmd = 'ip l2tp add session name {ifname}' diff --git a/python/vyos/ifconfig/vrrp.py b/python/vyos/ifconfig/vrrp.py index d3e9d5df2..b522cc1ab 100644 --- a/python/vyos/ifconfig/vrrp.py +++ b/python/vyos/ifconfig/vrrp.py @@ -92,11 +92,14 @@ class VRRP(object): try: # send signal to generate the configuration file pid = util.read_file(cls.location['pid']) - os.kill(int(pid), cls._signal[what]) + util.wait_for_file_write_complete(fname, + pre_hook=(lambda: os.kill(int(pid), cls._signal[what])), + timeout=30) - # should look for file size change? - sleep(0.2) return util.read_file(fname) + except OSError: + # raised by vyos.util.read_file + raise VRRPNoData("VRRP data is not available (wait time exceeded)") except FileNotFoundError: raise VRRPNoData("VRRP data is not available (process not running or no active groups)") except Exception: diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index a217d28ea..470ebbff3 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -33,7 +33,7 @@ class VTIIf(Interface): # - https://man7.org/linux/man-pages/man8/ip-link.8.html # - https://man7.org/linux/man-pages/man8/ip-tunnel.8.html mapping = { - 'source_interface' : 'dev', + 'source_interface' : 'dev', } if_id = self.ifname.lstrip('vti') @@ -50,8 +50,3 @@ class VTIIf(Interface): self._cmd(cmd.format(**self.config)) self.set_interface('admin_state', 'down') - - def set_admin_state(self, state): - # function is not implemented for VTI interfaces as this is entirely - # handled by the ipsec up/down scripts - pass diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index e5b9c4408..c4cf2fbbf 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -95,7 +95,7 @@ class WireGuardOperational(Operational): for peer in c.list_effective_nodes(["peer"]): if wgdump['peers']: - pubkey = c.return_effective_value(["peer", peer, "pubkey"]) + pubkey = c.return_effective_value(["peer", peer, "public_key"]) if pubkey in wgdump['peers']: wgpeer = wgdump['peers'][pubkey] @@ -194,11 +194,15 @@ class WireGuardIf(Interface): peer = config['peer_remove'][tmp] peer['ifname'] = config['ifname'] - cmd = 'wg set {ifname} peer {pubkey} remove' + cmd = 'wg set {ifname} peer {public_key} remove' self._cmd(cmd.format(**peer)) + config['private_key_file'] = '/tmp/tmp.wireguard.key' + with open(config['private_key_file'], 'w') as f: + f.write(config['private_key']) + # Wireguard base command is identical for every peer - base_cmd = 'wg set {ifname} private-key {private_key}' + base_cmd = 'wg set {ifname} private-key {private_key_file}' if 'port' in config: base_cmd += ' listen-port {port}' if 'fwmark' in config: @@ -210,7 +214,7 @@ class WireGuardIf(Interface): peer = config['peer'][tmp] # start of with a fresh 'wg' command - cmd = base_cmd + ' peer {pubkey}' + cmd = base_cmd + ' peer {public_key}' # If no PSK is given remove it by using /dev/null - passing keys via # the shell (usually bash) is considered insecure, thus we use a file diff --git a/python/vyos/pki.py b/python/vyos/pki.py index 1c6282d84..68ad73bf2 100644 --- a/python/vyos/pki.py +++ b/python/vyos/pki.py @@ -43,6 +43,8 @@ CSR_BEGIN='-----BEGIN CERTIFICATE REQUEST-----\n' CSR_END='\n-----END CERTIFICATE REQUEST-----' 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}-----' # Print functions @@ -227,6 +229,9 @@ def wrap_crl(raw_data): def wrap_dh_parameters(raw_data): return DH_BEGIN + raw_data + DH_END +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): diff --git a/python/vyos/template.py b/python/vyos/template.py index f03fd7ee7..08a5712af 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -29,13 +29,17 @@ _FILTERS = {} # reuse Environments with identical settings to improve performance @functools.lru_cache(maxsize=2) -def _get_environment(): +def _get_environment(location=None): + if location is None: + loc_loader=FileSystemLoader(directories["templates"]) + else: + loc_loader=FileSystemLoader(location) env = Environment( # Don't check if template files were modified upon re-rendering auto_reload=False, # Cache up to this number of templates for quick re-rendering cache_size=100, - loader=FileSystemLoader(directories["templates"]), + loader=loc_loader, trim_blocks=True, ) env.filters.update(_FILTERS) @@ -63,7 +67,7 @@ def register_filter(name, func=None): return func -def render_to_string(template, content, formater=None): +def render_to_string(template, content, formater=None, location=None): """Render a template from the template directory, raise on any errors. :param template: the path to the template relative to the template folder @@ -78,7 +82,7 @@ def render_to_string(template, content, formater=None): package is build (recovering the load time and overhead caused by having the file out of the code). """ - template = _get_environment().get_template(template) + template = _get_environment(location).get_template(template) rendered = template.render(content) if formater is not None: rendered = formater(rendered) @@ -93,6 +97,7 @@ def render( permission=None, user=None, group=None, + location=None, ): """Render a template from the template directory to a file, raise on any errors. @@ -109,7 +114,7 @@ def render( # As we are opening the file with 'w', we are performing the rendering before # calling open() to not accidentally erase the file if rendering fails - rendered = render_to_string(template, content, formater) + rendered = render_to_string(template, content, formater, location) # Write to file with open(destination, "w") as file: @@ -433,3 +438,38 @@ def get_esp_ike_cipher(group_config): ciphers.append(tmp) return ciphers + +@register_filter('get_uuid') +def get_uuid(interface): + """ Get interface IP addresses""" + from uuid import uuid1 + return uuid1() + +openvpn_translate = { + 'des': 'des-cbc', + '3des': 'des-ede3-cbc', + 'bf128': 'bf-cbc', + 'bf256': 'bf-cbc', + 'aes128gcm': 'aes-128-gcm', + 'aes128': 'aes-128-cbc', + 'aes192gcm': 'aes-192-gcm', + 'aes192': 'aes-192-cbc', + 'aes256gcm': 'aes-256-gcm', + 'aes256': 'aes-256-cbc' +} + +@register_filter('openvpn_cipher') +def get_openvpn_cipher(cipher): + if cipher in openvpn_translate: + return openvpn_translate[cipher].upper() + return cipher.upper() + +@register_filter('openvpn_ncp_ciphers') +def get_openvpn_ncp_ciphers(ciphers): + out = [] + for cipher in ciphers: + if cipher in openvpn_translate: + out.append(openvpn_translate[cipher]) + else: + out.append(cipher) + return ':'.join(out).upper() diff --git a/python/vyos/util.py b/python/vyos/util.py index 5a96013ea..59f9f1c44 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -515,6 +515,7 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl from inotify.adapters import Inotify from time import time + from time import sleep time_start = time() @@ -530,11 +531,14 @@ def wait_for_inotify(file_path, pre_hook=None, event_type=None, timeout=None, sl # the file failed to have been written to and closed within the timeout raise OSError("Waiting for file {} to be written has failed".format(file_path)) + # Most such events don't take much time, so it's better to check right away + # and sleep later. if event is not None: (_, type_names, path, filename) = event if filename == os.path.basename(file_path): if event_type in type_names: return + sleep(sleep_interval) def wait_for_file_write_complete(file_path, pre_hook=None, timeout=None, sleep_interval=0.1): """ Waits for a process to close a file after opening it in write mode. """ @@ -800,3 +804,9 @@ def make_incremental_progressbar(increment: float): # Ignore further calls. while True: yield + +def is_systemd_service_running(service): + """ Test is a specified systemd service is actually running. + Returns True if service is running, false otherwise. """ + tmp = run(f'systemctl is-active --quiet {service}') + return bool((tmp == 0)) diff --git a/scripts/override-default b/scripts/override-default index c8a0ff1da..0c49087c8 100755 --- a/scripts/override-default +++ b/scripts/override-default @@ -27,6 +27,7 @@ import sys import glob import logging +from copy import deepcopy from lxml import etree debug = False @@ -60,30 +61,55 @@ def override_element(l: list): for el in parents: el.getparent().remove(el) +def merge_remaining(l: list, elementtree): + """ + Merge (now) single leaf node containing 'defaultValue' with leaf nodes + of same path and no 'defaultValue'. + """ + for p in l: + p = p.split() + path_str = f'/interfaceDefinition/*' + path_list = [] + for i in range(len(p)): + path_list.append(f'[@name="{p[i]}"]') + path_str += '/children/*'.join(path_list) + rp = elementtree.xpath(path_str) + if len(rp) > 1: + for el in rp[1:]: + # in practice there will only be one child of the path, + # either defaultValue or Properties, since + # override_element() has already run + for child in el: + rp[0].append(deepcopy(child)) + el.getparent().remove(el) + def collect_and_override(dir_name): """ - Collect elements with defaultValue tag into dictionary indexed by tuple - of (name: str, ancestor path: str). + Collect elements with defaultValue tag into dictionary indexed by name + attributes of ancestor path. """ for fname in glob.glob(f'{dir_name}/*.xml'): tree = etree.parse(fname) root = tree.getroot() defv = {} - xpath_str = f'//defaultValue' + xpath_str = '//defaultValue' xp = tree.xpath(xpath_str) for element in xp: ap = element.xpath('ancestor::*[@name]') ap_name = [el.get("name") for el in ap] - ap_path_str = ' '.join(ap_name[:-1]) - defv.setdefault((ap_name[-1], ap_path_str), []).append(element) + ap_path_str = ' '.join(ap_name) + defv.setdefault(ap_path_str, []).append(element) for k, v in defv.items(): if len(v) > 1: - logger.info(f"overridding default in {k[0]}, path '{k[1]}'") + logger.info(f"overridding default in path '{k}'") override_element(v) + to_merge = list(defv) + merge_remaining(to_merge, tree) + revised_str = etree.tostring(root, encoding='unicode', pretty_print=True) with open(f'{fname}', 'w') as f: diff --git a/smoketest/configs/bgp-azure-ipsec-gateway b/smoketest/configs/bgp-azure-ipsec-gateway index 0862531fd..ddcd459ae 100644 --- a/smoketest/configs/bgp-azure-ipsec-gateway +++ b/smoketest/configs/bgp-azure-ipsec-gateway @@ -307,6 +307,7 @@ system { } vpn { ipsec { + auto-update 120 esp-group ESP-AZURE { compression disable lifetime 27000 @@ -341,35 +342,35 @@ vpn { log-modes ike } site-to-site { - peer 51.105.0.2 { + peer 51.105.0.1 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure } connection-type respond + default-esp-group ESP-AZURE ike-group IKE-AZURE ikev2-reauth inherit local-address 192.0.2.189 vti { bind vti51 - esp-group ESP-AZURE } } - peer 51.105.0.3 { + peer 51.105.0.2 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure } connection-type respond + default-esp-group ESP-AZURE ike-group IKE-AZURE ikev2-reauth inherit local-address 192.0.2.189 vti { bind vti52 - esp-group ESP-AZURE } } - peer 51.105.0.246 { + peer 51.105.0.3 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure @@ -383,7 +384,7 @@ vpn { esp-group ESP-AZURE } } - peer 51.105.0.247 { + peer 51.105.0.4 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure @@ -397,7 +398,7 @@ vpn { esp-group ESP-AZURE } } - peer 51.105.0.18 { + peer 51.105.0.5 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure @@ -411,7 +412,7 @@ vpn { esp-group ESP-AZURE } } - peer 51.105.0.19 { + peer 51.105.0.6 { authentication { mode pre-shared-secret pre-shared-secret averysecretpsktowardsazure diff --git a/smoketest/configs/pki-ipsec b/smoketest/configs/pki-ipsec index 5025117f7..6fc239d27 100644 --- a/smoketest/configs/pki-ipsec +++ b/smoketest/configs/pki-ipsec @@ -105,6 +105,33 @@ vpn { } } } + l2tp { + remote-access { + authentication { + local-users { + username alice { + password notsecure + } + } + mode local + } + client-ip-pool { + start 192.168.255.2 + stop 192.168.255.254 + } + ipsec-settings { + authentication { + mode x509 + x509 { + ca-cert-file /config/auth/ovpn_test_ca.pem + server-cert-file /config/auth/ovpn_test_server.pem + server-key-file /config/auth/ovpn_test_server.key + } + } + } + outside-address 192.168.150.1 + } + } rsa-keys { local-key { file /config/auth/ovpn_test_server.key diff --git a/smoketest/configs/pki-misc b/smoketest/configs/pki-misc new file mode 100644 index 000000000..c90226a2a --- /dev/null +++ b/smoketest/configs/pki-misc @@ -0,0 +1,98 @@ +interfaces { + ethernet eth0 { + address 192.168.150.1/24 + } +} +service { + https { + certificates { + system-generated-certificate { + lifetime 365 + } + } + } +} +system { + config-management { + commit-revisions 100 + } + console { + device ttyS0 { + speed 115200 + } + } + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/ + plaintext-password "" + } + } + } + ntp { + server time1.vyos.net { + } + server time2.vyos.net { + } + server time3.vyos.net { + } + } + syslog { + global { + facility all { + level info + } + facility protocols { + level debug + } + } + } +} +vpn { + openconnect { + authentication { + local-users { + username test { + password test + } + } + mode local + } + network-settings { + client-ip-settings { + subnet 192.168.160.0/24 + } + } + ssl { + ca-cert-file /config/auth/ovpn_test_ca.pem + cert-file /config/auth/ovpn_test_server.pem + key-file /config/auth/ovpn_test_server.key + } + } + sstp { + authentication { + local-users { + username test { + password test + } + } + mode local + protocols mschap-v2 + } + client-ip-pool { + subnet 192.168.170.0/24 + } + gateway-address 192.168.150.1 + ssl { + ca-cert-file /config/auth/ovpn_test_ca.pem + cert-file /config/auth/ovpn_test_server.pem + key-file /config/auth/ovpn_test_server.key + } + } +} + + +// Warning: Do not remove the following line. +// vyos-config-version: "bgp@1:broadcast-relay@1:cluster@1:config-management@1:conntrack@2:conntrack-sync@2:dhcp-relay@2:dhcp-server@5:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@6:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:nat66@1:ntp@1:policy@1:pppoe-server@5:pptp@2:qos@1:quagga@9:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrf@2:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.4-rolling-202106290839 diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index a31d75423..a9cdab16a 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -25,9 +25,9 @@ from vyos.util import cmd from vyos.util import process_named_running from vyos.util import read_file -ca_cert = '/config/auth/eapol_test_ca.pem' -ssl_cert = '/config/auth/eapol_test_server.pem' -ssl_key = '/config/auth/eapol_test_server.key' +pki_path = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' def get_wpa_supplicant_value(interface, key): tmp = read_file(f'/run/wpa_supplicant/{interface}.conf') @@ -66,6 +66,8 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): def tearDown(self): + self.cli_delete(pki_path) + for interface in self._interfaces: # when using a dedicated interface to test via TEST_ETH environment # variable only this one will be cleared in the end - usable to test @@ -149,11 +151,14 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.cli_commit() def test_eapol_support(self): + self.cli_set(pki_path + ['ca', 'eapol', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'eapol', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'eapol', 'private', 'key', key_data]) + for interface in self._interfaces: # Enable EAPoL - self.cli_set(self._base_path + [interface, 'eapol', 'ca-cert-file', ca_cert]) - self.cli_set(self._base_path + [interface, 'eapol', 'cert-file', ssl_cert]) - self.cli_set(self._base_path + [interface, 'eapol', 'key-file', ssl_key]) + self.cli_set(self._base_path + [interface, 'eapol', 'ca-certificate', 'eapol']) + self.cli_set(self._base_path + [interface, 'eapol', 'certificate', 'eapol']) self.cli_commit() @@ -172,35 +177,17 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): self.assertEqual('0', tmp) tmp = get_wpa_supplicant_value(interface, 'ca_cert') - self.assertEqual(f'"{ca_cert}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_ca.pem"', tmp) tmp = get_wpa_supplicant_value(interface, 'client_cert') - self.assertEqual(f'"{ssl_cert}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.pem"', tmp) tmp = get_wpa_supplicant_value(interface, 'private_key') - self.assertEqual(f'"{ssl_key}"', tmp) + self.assertEqual(f'"/run/wpa_supplicant/{interface}_cert.key"', tmp) mac = read_file(f'/sys/class/net/{interface}/address') tmp = get_wpa_supplicant_value(interface, 'identity') self.assertEqual(f'"{mac}"', tmp) if __name__ == '__main__': - # Our SSL certificates need a subject ... - subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ - 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' - - if not (os.path.isfile(ssl_key) and os.path.isfile(ssl_cert)): - # Generate mandatory SSL certificate - tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\ - f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}' - cmd(tmp) - - if not os.path.isfile(ca_cert): - # Generate "CA" - tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} -subj {subject}' - cmd(tmp) - - for file in [ca_cert, ssl_cert, ssl_key]: - cmd(f'sudo chown radius_priv_user:vyattacfg {file}') - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_openvpn.py b/smoketest/scripts/cli/test_interfaces_openvpn.py index 68c61b98c..7ce1b9872 100755 --- a/smoketest/scripts/cli/test_interfaces_openvpn.py +++ b/smoketest/scripts/cli/test_interfaces_openvpn.py @@ -37,12 +37,11 @@ from vyos.template import netmask_from_cidr PROCESS_NAME = 'openvpn' base_path = ['interfaces', 'openvpn'] -ca_cert = '/config/auth/ovpn_test_ca.pem' -ssl_cert = '/config/auth/ovpn_test_server.pem' -ssl_key = '/config/auth/ovpn_test_server.key' -dh_pem = '/config/auth/ovpn_test_dh.pem' -s2s_key = '/config/auth/ovpn_test_site2site.key' -auth_key = '/config/auth/ovpn_test_tls_auth.key' + +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' +dh_data = 'MIIBCAKCAQEApzGAPcQlLJiOyfGZgl1qxNgufXkdpjG7lMaOrO4TGr1giFe3jIFOFxJNC/G9Dn+KSukaWssVVR+Jwr/JesZFPawihS03wC7cZsccykNRIjiteqJDwYJZUHieOxyCuCeY4pqOUCl1uswRGjLvIFtwynpnXKKuz2YtjNifma90PEgv/vVWKix+Q0TAbdbzJzO5xp8UVn9DuYfSr10k3LbDqDM7w5ezHZxFk24S5pN/yoOpdbxB8TS67q3IYXxR3F+RseKu4J3AvkxXSP1j7COXddPpLnvbJT/SW8NrjuC/n0eKGvmeyqNv108Y89jnT79MxMMRQk66iwlsd1m4pa/OYwIBAg==' +ovpn_key_data = '443f2a710ac411c36894b2531e62c4550b079b8f3f08997f4be57c64abfdaaa431d2396b01ecec3a2c0618959e8186d99f489742d25673ffb3268841ebb2e7042a2daabe584e79d51d2b1d7409bf8840f7e42efa3e660a521719b04ee88b9043e6315ae12da7c9abd55f67eeed71a9ee8c6e163b5d2661fc332cf90cb45658b4adf892f79537d37d3a3d90da283ce885adf325ffd2b5be92067cdf0345c7712c9d36b642c170351b6d9ce9f6230c7a2617b0c181121bce7d5373404fb68e65210b36e6d40ef2769cf8990503859f6f2db3c85ba74420430a6250d6a74ca51ece4b85124bfdfec0c8a530cefa7350378d81a4539f74bed832a902ae4798142e4a' remote_port = '1194' protocol = 'udp' @@ -65,6 +64,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(['interfaces', 'dummy', dummy_if, 'address', '192.0.2.1/32']) self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) + self.cli_set(['pki', 'ca', 'ovpn_test', 'certificate', cert_data]) + self.cli_set(['pki', 'certificate', 'ovpn_test', 'certificate', cert_data]) + self.cli_set(['pki', 'certificate', 'ovpn_test', 'private', 'key', key_data]) + self.cli_set(['pki', 'dh', 'ovpn_test', 'parameters', dh_data]) + self.cli_set(['pki', 'openvpn', 'shared-secret', 'ovpn_test', 'key', ovpn_key_data]) + def tearDown(self): self.cli_delete(base_path) self.cli_delete(['interfaces', 'dummy', dummy_if]) @@ -101,25 +106,24 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_set(path + ['remote-host', '192.0.9.9']) - # check validate() - cannot specify "tls dh-file" in client mode - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + # check validate() - cannot specify "tls dh-params" in client mode + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(path + ['tls']) - # check validate() - must specify one of "shared-secret-key-file" and "tls" + # check validate() - must specify one of "shared-secret-key" and "tls" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['shared-secret-key-file', s2s_key]) + self.cli_set(path + ['shared-secret-key', 'ovpn_test']) - # check validate() - must specify one of "shared-secret-key-file" and "tls" + # check validate() - must specify one of "shared-secret-key" and "tls" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(path + ['shared-secret-key-file', s2s_key]) + self.cli_delete(path + ['shared-secret-key', 'ovpn_test']) - self.cli_set(path + ['tls', 'ca-cert-file', ca_cert]) - self.cli_set(path + ['tls', 'cert-file', ssl_cert]) - self.cli_set(path + ['tls', 'key-file', ssl_key]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) # check validate() - can not have auth username without a password self.cli_set(path + ['authentication', 'username', 'vyos']) @@ -152,9 +156,8 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['protocol', protocol]) self.cli_set(path + ['remote-host', remote_host]) self.cli_set(path + ['remote-port', remote_port]) - self.cli_set(path + ['tls', 'ca-cert-file', ca_cert]) - self.cli_set(path + ['tls', 'cert-file', ssl_cert]) - self.cli_set(path + ['tls', 'key-file', ssl_key]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) self.cli_set(path + ['vrf', vrf_name]) self.cli_set(path + ['authentication', 'username', interface+'user']) self.cli_set(path + ['authentication', 'password', interface+'secretpw']) @@ -176,12 +179,12 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'remote {remote_host}', config) self.assertIn(f'persist-tun', config) self.assertIn(f'auth {auth_hash}', config) - self.assertIn(f'cipher aes-256-cbc', config) + self.assertIn(f'cipher AES-256-CBC', config) # TLS options - self.assertIn(f'ca {ca_cert}', config) - self.assertIn(f'cert {ssl_cert}', config) - self.assertIn(f'key {ssl_key}', config) + self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config) + self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config) + self.assertIn(f'key /run/openvpn/{interface}_cert.key', config) self.assertTrue(process_named_running(PROCESS_NAME)) self.assertEqual(get_vrf(interface), vrf_name) @@ -228,11 +231,11 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(path + ['remote-host']) - # check validate() - must specify "tls dh-file" when not using EC keys + # check validate() - must specify "tls dh-params" when not using EC keys # in server mode with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) # check validate() - must specify "server subnet" or add interface to # bridge in server mode @@ -251,20 +254,15 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(path + ['server', 'subnet', '100.64.0.0/10']) - # check validate() - must specify "tls ca-cert-file" - with self.assertRaises(ConfigSessionError): - self.cli_commit() - self.cli_set(path + ['tls', 'ca-cert-file', ca_cert]) - - # check validate() - must specify "tls cert-file" + # check validate() - must specify "tls ca-certificate" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['tls', 'cert-file', ssl_cert]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) - # check validate() - must specify "tls key-file" + # check validate() - must specify "tls certificate" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['tls', 'key-file', ssl_key]) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) # check validate() - cannot specify "tls role" in client-server mode' self.cli_set(path + ['tls', 'role', 'active']) @@ -272,7 +270,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() # check validate() - cannot specify "tls role" in client-server mode' - self.cli_set(path + ['tls', 'auth-file', auth_key]) + self.cli_set(path + ['tls', 'auth-key', 'ovpn_test']) with self.assertRaises(ConfigSessionError): self.cli_commit() @@ -282,11 +280,11 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(path + ['protocol']) - # check validate() - cannot specify "tls dh-file" when "tls role" is "active" - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + # check validate() - cannot specify "tls dh-params" when "tls role" is "active" + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(path + ['tls', 'dh-file']) + self.cli_delete(path + ['tls', 'dh-params']) # Now test the other path with tls role passive self.cli_set(path + ['tls', 'role', 'passive']) @@ -297,10 +295,10 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_delete(path + ['protocol']) - # check validate() - must specify "tls dh-file" when "tls role" is "passive" + # check validate() - must specify "tls dh-params" when "tls role" is "passive" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) self.cli_commit() @@ -338,10 +336,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['server', 'client', 'client1', 'subnet', route]) self.cli_set(path + ['replace-default-route']) - self.cli_set(path + ['tls', 'ca-cert-file', ca_cert]) - self.cli_set(path + ['tls', 'cert-file', ssl_cert]) - self.cli_set(path + ['tls', 'key-file', ssl_key]) - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) self.cli_set(path + ['vrf', vrf_name]) self.cli_commit() @@ -367,17 +364,17 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'persist-key', config) self.assertIn(f'proto udp', config) # default protocol self.assertIn(f'auth {auth_hash}', config) - self.assertIn(f'cipher aes-192-cbc', config) + self.assertIn(f'cipher AES-192-CBC', config) self.assertIn(f'topology subnet', config) self.assertIn(f'lport {port}', config) self.assertIn(f'push "redirect-gateway def1"', config) self.assertIn(f'keepalive 5 25', config) # TLS options - self.assertIn(f'ca {ca_cert}', config) - self.assertIn(f'cert {ssl_cert}', config) - self.assertIn(f'key {ssl_key}', config) - self.assertIn(f'dh {dh_pem}', config) + self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config) + self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config) + self.assertIn(f'key /run/openvpn/{interface}_cert.key', config) + self.assertIn(f'dh /run/openvpn/{interface}_dh.pem', config) # IP pool configuration netmask = IPv4Network(subnet).netmask @@ -425,10 +422,9 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['replace-default-route']) self.cli_set(path + ['keep-alive', 'failure-count', '10']) self.cli_set(path + ['keep-alive', 'interval', '5']) - self.cli_set(path + ['tls', 'ca-cert-file', ca_cert]) - self.cli_set(path + ['tls', 'cert-file', ssl_cert]) - self.cli_set(path + ['tls', 'key-file', ssl_key]) - self.cli_set(path + ['tls', 'dh-file', dh_pem]) + self.cli_set(path + ['tls', 'ca-certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'certificate', 'ovpn_test']) + self.cli_set(path + ['tls', 'dh-params', 'ovpn_test']) self.cli_set(path + ['vrf', vrf_name]) self.cli_commit() @@ -448,17 +444,17 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'persist-key', config) self.assertIn(f'proto udp', config) # default protocol self.assertIn(f'auth {auth_hash}', config) - self.assertIn(f'cipher aes-192-cbc', config) + self.assertIn(f'cipher AES-192-CBC', config) self.assertIn(f'topology net30', config) self.assertIn(f'lport {port}', config) self.assertIn(f'push "redirect-gateway def1"', config) self.assertIn(f'keepalive 5 50', config) # TLS options - self.assertIn(f'ca {ca_cert}', config) - self.assertIn(f'cert {ssl_cert}', config) - self.assertIn(f'key {ssl_key}', config) - self.assertIn(f'dh {dh_pem}', config) + self.assertIn(f'ca /run/openvpn/{interface}_ca.pem', config) + self.assertIn(f'cert /run/openvpn/{interface}_cert.pem', config) + self.assertIn(f'key /run/openvpn/{interface}_cert.key', config) + self.assertIn(f'dh /run/openvpn/{interface}_dh.pem', config) # IP pool configuration netmask = IPv4Network(subnet).netmask @@ -530,10 +526,10 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(path + ['remote-address', '2001:db8:ffff::2']) - # check validate() - Must specify one of "shared-secret-key-file" and "tls" + # check validate() - Must specify one of "shared-secret-key" and "tls" with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(path + ['shared-secret-key-file', s2s_key]) + self.cli_set(path + ['shared-secret-key', 'ovpn_test']) self.cli_commit() @@ -565,7 +561,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['mode', 'site-to-site']) self.cli_set(path + ['local-port', port]) self.cli_set(path + ['remote-port', port]) - self.cli_set(path + ['shared-secret-key-file', s2s_key]) + self.cli_set(path + ['shared-secret-key', 'ovpn_test']) self.cli_set(path + ['remote-address', remote_address]) self.cli_set(path + ['vrf', vrf_name]) @@ -589,7 +585,7 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): self.assertIn(f'ifconfig {local_address} {local_address_subnet}', config) self.assertIn(f'dev {interface}', config) - self.assertIn(f'secret {s2s_key}', config) + self.assertIn(f'secret /run/openvpn/{interface}_shared.key', config) self.assertIn(f'lport {port}', config) self.assertIn(f'rport {port}', config) @@ -609,37 +605,4 @@ class TestInterfacesOpenVPN(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - # Our SSL certificates need a subject ... - subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ - 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' - - if not (os.path.isfile(ssl_key) and os.path.isfile(ssl_cert)): - # Generate mandatory SSL certificate - tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\ - f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}' - cmd(tmp) - - if not os.path.isfile(ca_cert): - # Generate "CA" - tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} -subj {subject}' - cmd(tmp) - - if not os.path.isfile(dh_pem): - # Generate "DH" key - tmp = f'openssl dhparam -out {dh_pem} 2048' - cmd(tmp) - - if not os.path.isfile(s2s_key): - # Generate site-2-site key - tmp = f'openvpn --genkey --secret {s2s_key}' - cmd(tmp) - - if not os.path.isfile(auth_key): - # Generate TLS auth key - tmp = f'openvpn --genkey --secret {auth_key}' - cmd(tmp) - - for file in [ca_cert, ssl_cert, ssl_key, dh_pem, s2s_key, auth_key]: - cmd(f'sudo chown openvpn:openvpn {file}') - unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_wireguard.py b/smoketest/scripts/cli/test_interfaces_wireguard.py index d31ec0332..3707eaac3 100755 --- a/smoketest/scripts/cli/test_interfaces_wireguard.py +++ b/smoketest/scripts/cli/test_interfaces_wireguard.py @@ -21,11 +21,6 @@ from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSession from vyos.configsession import ConfigSessionError - -# Generate WireGuard default keypair -if not os.path.isdir('/config/auth/wireguard/default'): - os.system('sudo /usr/libexec/vyos/op_mode/wireguard.py --genkey') - base_path = ['interfaces', 'wireguard'] class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): @@ -42,12 +37,15 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): # Create WireGuard interfaces with associated peers for intf in self._interfaces: peer = 'foo-' + intf + privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc=' psk = 'u2xdA70hkz0S1CG0dZlOh0aq2orwFXRIVrKo4DCvHgM=' pubkey = 'n6ZZL7ph/QJUJSUUTyu19c77my1dRCDHkMzFQUO9Z3A=' for addr in self._test_addr: self.cli_set(base_path + [intf, 'address', addr]) + self.cli_set(base_path + [intf, 'private-key', privkey]) + self.cli_set(base_path + [intf, 'peer', peer, 'address', '127.0.0.1']) self.cli_set(base_path + [intf, 'peer', peer, 'port', '1337']) @@ -57,7 +55,7 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + [intf, 'peer', peer, 'allowed-ips', ip]) self.cli_set(base_path + [intf, 'peer', peer, 'preshared-key', psk]) - self.cli_set(base_path + [intf, 'peer', peer, 'pubkey', pubkey]) + self.cli_set(base_path + [intf, 'peer', peer, 'public-key', pubkey]) self.cli_commit() self.assertTrue(os.path.isdir(f'/sys/class/net/{intf}')) @@ -68,17 +66,19 @@ class WireGuardInterfaceTest(VyOSUnitTestSHIM.TestCase): # Remove one of the configured peers. interface = 'wg0' port = '12345' + privkey = '6ISOkASm6VhHOOSz/5iIxw+Q9adq9zA17iMM4X40dlc=' pubkey_1 = 'n1CUsmR0M2LUUsyicBd6blZICwUqqWWHbu4ifZ2/9gk=' pubkey_2 = 'ebFx/1G0ti8tvuZd94sEIosAZZIznX+dBAKG/8DFm0I=' self.cli_set(base_path + [interface, 'address', '172.16.0.1/24']) + self.cli_set(base_path + [interface, 'private-key', privkey]) - self.cli_set(base_path + [interface, 'peer', 'PEER01', 'pubkey', pubkey_1]) + self.cli_set(base_path + [interface, 'peer', 'PEER01', 'public-key', pubkey_1]) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'port', port]) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'allowed-ips', '10.205.212.10/32']) self.cli_set(base_path + [interface, 'peer', 'PEER01', 'address', '192.0.2.1']) - self.cli_set(base_path + [interface, 'peer', 'PEER02', 'pubkey', pubkey_2]) + self.cli_set(base_path + [interface, 'peer', 'PEER02', 'public-key', pubkey_2]) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'port', port]) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'allowed-ips', '10.205.212.11/32']) self.cli_set(base_path + [interface, 'peer', 'PEER02', 'address', '192.0.2.2']) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index a1b3356ce..c3a2ffbf9 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -694,5 +694,21 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {interface} activate', frrconfig) self.assertIn(f' exit-address-family', frrconfig) + def test_bgp_13_solo(self): + remote_asn = str(int(ASN) + 150) + neighbor = '192.0.2.55' + + self.cli_set(base_path + ['local-as', ASN]) + self.cli_set(base_path + ['neighbor', neighbor, 'remote-as', remote_asn]) + self.cli_set(base_path + ['neighbor', neighbor, 'solo']) + + # commit changes + self.cli_commit() + + # Verify FRR bgpd configuration + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {neighbor} solo', frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2)
\ No newline at end of file diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py index 8389e42e9..aa0ac268d 100755 --- a/smoketest/scripts/cli/test_protocols_nhrp.py +++ b/smoketest/scripts/cli/test_protocols_nhrp.py @@ -68,7 +68,7 @@ class TestProtocolsNHRP(VyOSUnitTestSHIM.TestCase): self.cli_set(vpn_path + ["ike-group", "IKE-HUB", "proposal", "2", "hash", "sha1"]) # Profile - Not doing full DMVPN checks here, just want to verify the profile name in the output - self.cli_set(vpn_path + ["ipsec-interfaces", "interface", "eth0"]) + self.cli_set(vpn_path + ["interface", "eth0"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "mode", "pre-shared-secret"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "authentication", "pre-shared-secret", "secret"]) self.cli_set(vpn_path + ["profile", "NHRPVPN", "bind", "tunnel", "tun100"]) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 23e5f2678..59862ca3d 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -14,6 +14,8 @@ # 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 logging +import sys import unittest from base_vyostest_shim import VyOSUnitTestSHIM @@ -27,6 +29,8 @@ base_path = ['protocols', 'ospf'] route_map = 'foo-bar-baz10' +log = logging.getLogger('TestProtocolsOSPF') + class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def setUp(self): self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) @@ -202,11 +206,11 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for interface in interfaces: self.assertIn(f' no passive-interface {interface}', frrconfig) # default except: - tmp = cmd('tail -n 250 /var/log/messages') - print(tmp) - tmp = cmd('vtysh -c "show run"') - print(tmp) - self.fail('Now we can hopefully see why OSPF fails') + log.debug(frrconfig) + log.debug(cmd('sudo dmesg')) + log.debug(cmd('sudo cat /var/log/messages')) + log.debug(cmd('vtysh -c "show run"')) + self.fail('Now we can hopefully see why OSPF fails!') def test_ospf_08_redistribute(self): metric = '15' @@ -346,4 +350,5 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): self.assertNotIn(zebra_route_map, frrconfig) if __name__ == '__main__': + logging.basicConfig(stream=sys.stderr, level=logging.DEBUG) unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index 8212e9469..6d334a9f8 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -84,6 +84,7 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): self.assertIn(f'rpki cache {peer} {port} preference {preference}', frrconfig) def test_rpki_ssh(self): + self.skipTest('Currently untested, see: https://github.com/FRRouting/frr/issues/7978') polling = '7200' cache = { '192.0.2.3' : { diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 3ed7655e9..3af63636a 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -22,14 +22,20 @@ from vyos.util import run base_path = ['service', 'https'] +pki_base = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' + class TestHTTPSService(VyOSUnitTestSHIM.TestCase): def setUp(self): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) self.cli_delete(base_path) + self.cli_delete(pki_base) def tearDown(self): self.cli_delete(base_path) + self.cli_delete(pki_base) self.cli_commit() def test_default(self): @@ -56,5 +62,16 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): ret = run('sudo /usr/sbin/nginx -t') self.assertEqual(ret, 0) + def test_certificate(self): + self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data]) + self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data]) + + self.cli_set(base_path + ['certificates', 'certificate', 'test_https']) + + self.cli_commit() + + ret = run('sudo /usr/sbin/nginx -t') + self.assertEqual(ret, 0) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index b19c49c6e..26b4626c2 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -43,11 +43,10 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - def test_single(self): + def test_common(self): self.cli_set(base_path + ['prefix', '::/64', 'no-on-link-flag']) self.cli_set(base_path + ['prefix', '::/64', 'no-autonomous-flag']) self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) - self.cli_set(base_path + ['dnssl', '2001:db8::1234']) self.cli_set(base_path + ['other-config-flag']) # commit changes @@ -92,5 +91,28 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running('radvd')) + def test_dns(self): + nameserver = ['2001:db8::1', '2001:db8::2'] + dnssl = ['vyos.net', 'vyos.io'] + + self.cli_set(base_path + ['prefix', '::/64', 'valid-lifetime', 'infinity']) + self.cli_set(base_path + ['other-config-flag']) + + for ns in nameserver: + self.cli_set(base_path + ['name-server', ns]) + for sl in dnssl: + self.cli_set(base_path + ['dnssl', sl]) + + # commit changes + self.cli_commit() + + config = read_file(RADVD_CONF) + + tmp = 'RDNSS ' + ' '.join(nameserver) + ' {' + self.assertIn(tmp, config) + + tmp = 'DNSSL ' + ' '.join(dnssl) + ' {' + self.assertIn(tmp, config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py index aa97511e0..8327235fb 100755 --- a/smoketest/scripts/cli/test_system_login.py +++ b/smoketest/scripts/cli/test_system_login.py @@ -41,6 +41,17 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase): self.cli_commit() + def test_add_linux_system_user(self): + system_user = 'backup' + self.cli_set(base_path + ['user', system_user, 'authentication', 'plaintext-password', system_user]) + + # check validate() - can not add username which exists on the Debian + # base system (UID < 1000) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_delete(base_path + ['user', system_user]) + def test_system_login_user(self): # Check if user can be created and we can SSH to localhost self.cli_set(['service', 'ssh', 'port', '22']) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index fda8b74b1..a34387dc9 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -112,7 +112,7 @@ rgiyCHemtMepq57Pl1Nmj49eEA== class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): def setUp(self): - self.cli_set(base_path + ['ipsec-interfaces', 'interface', f'{interface}.{vif}']) + self.cli_set(base_path + ['interface', f'{interface}.{vif}']) # Set IKE/ESP Groups self.cli_set(base_path + ['esp-group', esp_group, 'proposal', '1', 'encryption', 'aes128']) diff --git a/smoketest/scripts/cli/test_vpn_openconnect.py b/smoketest/scripts/cli/test_vpn_openconnect.py index bf528c8b7..cad3b1182 100755 --- a/smoketest/scripts/cli/test_vpn_openconnect.py +++ b/smoketest/scripts/cli/test_vpn_openconnect.py @@ -23,25 +23,33 @@ from vyos.util import process_named_running OCSERV_CONF = '/run/ocserv/ocserv.conf' base_path = ['vpn', 'openconnect'] -cert = '/etc/ssl/certs/ssl-cert-snakeoil.pem' -cert_key = '/etc/ssl/private/ssl-cert-snakeoil.key' + +pki_path = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' class TestVpnOpenconnect(VyOSUnitTestSHIM.TestCase): def tearDown(self): # Delete vpn openconnect configuration + self.cli_delete(pki_path) self.cli_delete(base_path) self.cli_commit() def test_vpn(self): user = 'vyos_user' password = 'vyos_pass' + self.cli_delete(pki_path) self.cli_delete(base_path) + + self.cli_set(pki_path + ['ca', 'openconnect', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'openconnect', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'openconnect', 'private', 'key', key_data]) + self.cli_set(base_path + ["authentication", "local-users", "username", user, "password", password]) self.cli_set(base_path + ["authentication", "mode", "local"]) self.cli_set(base_path + ["network-settings", "client-ip-settings", "subnet", "192.0.2.0/24"]) - self.cli_set(base_path + ["ssl", "ca-cert-file", cert]) - self.cli_set(base_path + ["ssl", "cert-file", cert]) - self.cli_set(base_path + ["ssl", "key-file", cert_key]) + self.cli_set(base_path + ["ssl", "ca-certificate", 'openconnect']) + self.cli_set(base_path + ["ssl", "certificate", 'openconnect']) self.cli_commit() diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index 033338685..24673278b 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.py @@ -19,9 +19,9 @@ import unittest from base_accel_ppp_test import BasicAccelPPPTest from vyos.util import cmd -ca_cert = '/tmp/ca.crt' -ssl_cert = '/tmp/server.crt' -ssl_key = '/tmp/server.key' +pki_path = ['pki'] +cert_data = 'MIICFDCCAbugAwIBAgIUfMbIsB/ozMXijYgUYG80T1ry+mcwCgYIKoZIzj0EAwIwWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MB4XDTIxMDcyMDEyNDUxMloXDTI2MDcxOTEyNDUxMlowWTELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzESMBAGA1UEAwwJVnlPUyBUZXN0MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE01HrLcNttqq4/PtoMua8rMWEkOdBu7vP94xzDO7A8C92ls1v86eePy4QllKCzIw3QxBIoCuH2peGRfWgPRdFsKNhMF8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAYYwHQYDVR0lBBYwFAYIKwYBBQUHAwIGCCsGAQUFBwMBMB0GA1UdDgQWBBSu+JnU5ZC4mkuEpqg2+Mk4K79oeDAKBggqhkjOPQQDAgNHADBEAiBEFdzQ/Bc3LftzngrY605UhA6UprHhAogKgROv7iR4QgIgEFUxTtW3xXJcnUPWhhUFhyZoqfn8dE93+dm/LDnp7C0=' +key_data = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx2KMIuze7ucKUt/lBEB2wc03IxXyhRANCAATTUestw222qrj8+2gy5rysxYSQ50G7u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww' class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): def setUp(self): @@ -31,28 +31,21 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): self._chap_secrets = '/run/accel-pppd/sstp.chap-secrets' super().setUp() + def tearDown(self): + self.cli_delete(pki_path) + super().tearDown() + def basic_config(self): + self.cli_delete(pki_path) + self.cli_set(pki_path + ['ca', 'sstp', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'sstp', 'certificate', cert_data]) + self.cli_set(pki_path + ['certificate', 'sstp', 'private', 'key', key_data]) # SSL is mandatory - self.set(['ssl', 'ca-cert-file', ca_cert]) - self.set(['ssl', 'cert-file', ssl_cert]) - self.set(['ssl', 'key-file', ssl_key]) + self.set(['ssl', 'ca-certificate', 'sstp']) + self.set(['ssl', 'certificate', 'sstp']) self.set(['client-ip-pool', 'subnet', '192.0.2.0/24']) super().basic_config() if __name__ == '__main__': - # Our SSL certificates need a subject ... - subject = '/C=DE/ST=BY/O=VyOS/localityName=Cloud/commonName=vyos/' \ - 'organizationalUnitName=VyOS/emailAddress=maintainers@vyos.io/' - - # Generate mandatory SSL certificate - tmp = f'openssl req -newkey rsa:4096 -new -nodes -x509 -days 3650 '\ - f'-keyout {ssl_key} -out {ssl_cert} -subj {subject}' - cmd(tmp) - - # Generate "CA" - tmp = f'openssl req -new -x509 -key {ssl_key} -out {ca_cert} '\ - f'-subj {subject}' - cmd(tmp) - unittest.main(verbosity=2) diff --git a/src/conf_mode/containers.py b/src/conf_mode/containers.py index 21b47f42a..7544bd840 100755 --- a/src/conf_mode/containers.py +++ b/src/conf_mode/containers.py @@ -142,9 +142,9 @@ def verify(container): # Add new network if 'network' in container: - v4_prefix = 0 - v6_prefix = 0 for network, network_config in container['network'].items(): + v4_prefix = 0 + v6_prefix = 0 # If ipv4-prefix not defined for user-defined network if 'prefix' not in network_config: raise ConfigError(f'prefix for network "{net}" must be defined!') diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index a6e2d9c8c..be4380462 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2021 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 @@ -14,6 +14,7 @@ # 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 sys from copy import deepcopy @@ -23,13 +24,17 @@ import vyos.certbot_util from vyos.config import Config from vyos import ConfigError -from vyos.util import call +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render +from vyos.util import call from vyos import airbag airbag.enable() config_file = '/etc/nginx/sites-available/default' +cert_dir = '/etc/ssl/certs' +key_dir = '/etc/ssl/private' certbot_dir = vyos.defaults.directories['certbot'] # https config needs to coordinate several subsystems: api, certbot, @@ -56,12 +61,58 @@ def get_config(config=None): if not conf.exists('service https'): return None + https = conf.get_config_dict('service https', get_first_key=True) + + if https: + https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + + return https + +def verify(https): + if https is None: + return None + + if 'certificates' in https: + certificates = https['certificates'] + + if 'certificate' in certificates: + if not https['pki']: + raise ConfigError("PKI is not configured") + + cert_name = certificates['certificate'] + + if cert_name not in https['pki']['certificate']: + raise ConfigError("Invalid certificate on https configuration") + + pki_cert = https['pki']['certificate'][cert_name] + + if 'certificate' not in pki_cert: + raise ConfigError("Missing certificate on https configuration") + + if 'private' not in pki_cert or 'key' not in pki_cert['private']: + raise ConfigError("Missing certificate private key on https configuration") + + if 'certbot' in https['certificates']: + vhost_names = [] + for vh, vh_conf in https.get('virtual-host', {}).items(): + vhost_names += vh_conf.get('server-name', []) + domains = https['certificates']['certbot'].get('domain-name', []) + domains_found = [domain for domain in domains if domain in vhost_names] + if not domains_found: + raise ConfigError("At least one 'virtual-host <id> server-name' " + "matching the 'certbot domain-name' is required.") + return None + +def generate(https): + if https is None: + return None + server_block_list = [] - https_dict = conf.get_config_dict('service https', get_first_key=True) # organize by vhosts - vhost_dict = https_dict.get('virtual-host', {}) + vhost_dict = https.get('virtual-host', {}) if not vhost_dict: # no specified virtual hosts (server blocks); use default @@ -79,18 +130,30 @@ def get_config(config=None): # get certificate data - cert_dict = https_dict.get('certificates', {}) + cert_dict = https.get('certificates', {}) + + if 'certificate' in cert_dict: + cert_name = cert_dict['certificate'] + pki_cert = https['pki']['certificate'][cert_name] + + cert_path = os.path.join(cert_dir, f'{cert_name}.pem') + key_path = os.path.join(key_dir, f'{cert_name}.pem') + + with open(cert_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + with open(key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) - # self-signed certificate + vyos_cert_data = { + "crt": cert_path, + "key": key_path + } - vyos_cert_data = {} - if 'system-generated-certificate' in list(cert_dict): - vyos_cert_data = vyos.defaults.vyos_cert_data - if vyos_cert_data: for block in server_block_list: block['vyos_cert'] = vyos_cert_data - # letsencrypt certificate using certbot + # letsencrypt certificate using certbot certbot = False cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) @@ -110,15 +173,15 @@ def get_config(config=None): api_set = False api_data = {} - if 'api' in list(https_dict): + if 'api' in list(https): api_set = True api_data = vyos.defaults.api_data - api_settings = https_dict.get('api', {}) + api_settings = https.get('api', {}) if api_settings: port = api_settings.get('port', '') if port: api_data['port'] = port - vhosts = https_dict.get('api-restrict', {}).get('virtual-host', []) + vhosts = https.get('api-restrict', {}).get('virtual-host', []) if vhosts: api_data['vhost'] = vhosts[:] @@ -132,34 +195,16 @@ def get_config(config=None): if block['id'] in vhost_list: block['api'] = api_data - # return dict for use in template - - https = {'server_block_list' : server_block_list, - 'api_set': api_set, - 'certbot': certbot} - - return https - -def verify(https): - if https is None: - return None - - if https['certbot']: - for sb in https['server_block_list']: - if sb['certbot']: - return None - raise ConfigError("At least one 'virtual-host <id> server-name' " - "matching the 'certbot domain-name' is required.") - return None - -def generate(https): - if https is None: - return None - if 'server_block_list' not in https or not https['server_block_list']: https['server_block_list'] = [default_server_block] - render(config_file, 'https/nginx.default.tmpl', https) + data = { + 'server_block_list': server_block_list, + 'api_set': api_set, + 'certbot': certbot + } + + render(config_file, 'https/nginx.default.tmpl', data) return None diff --git a/src/conf_mode/interfaces-ethernet.py b/src/conf_mode/interfaces-ethernet.py index 378f400b8..78c24952b 100755 --- a/src/conf_mode/interfaces-ethernet.py +++ b/src/conf_mode/interfaces-ethernet.py @@ -32,6 +32,8 @@ from vyos.configverify import verify_vlan_config from vyos.configverify import verify_vrf from vyos.ethtool import Ethtool from vyos.ifconfig import EthernetIf +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import dict_search @@ -40,6 +42,7 @@ from vyos import airbag airbag.enable() # XXX: wpa_supplicant works on the source interface +cfg_dir = '/run/wpa_supplicant' wpa_suppl_conf = '/run/wpa_supplicant/{ifname}.conf' def get_config(config=None): @@ -52,8 +55,15 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'ethernet'] + + tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + ethernet = get_interface_dict(conf, base) + if 'deleted' not in ethernet: + ethernet['pki'] = tmp_pki + return ethernet def verify(ethernet): @@ -126,6 +136,27 @@ def generate(ethernet): if 'eapol' in ethernet: render(wpa_suppl_conf.format(**ethernet), 'ethernet/wpa_supplicant.conf.tmpl', ethernet) + + ifname = ethernet['ifname'] + cert_file_path = os.path.join(cfg_dir, f'{ifname}_cert.pem') + cert_key_path = os.path.join(cfg_dir, f'{ifname}_cert.key') + + cert_name = ethernet['eapol']['certificate'] + pki_cert = ethernet['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in ethernet['eapol']: + ca_cert_file_path = os.path.join(cfg_dir, f'{ifname}_ca.pem') + ca_cert_name = ethernet['eapol']['ca_certificate'] + pki_ca_cert = ethernet['pki']['ca'][cert_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) else: # delete configuration on interface removal if os.path.isfile(wpa_suppl_conf.format(**ethernet)): diff --git a/src/conf_mode/interfaces-loopback.py b/src/conf_mode/interfaces-loopback.py index 30a27abb4..193334443 100755 --- a/src/conf_mode/interfaces-loopback.py +++ b/src/conf_mode/interfaces-loopback.py @@ -45,8 +45,8 @@ def generate(loopback): return None def apply(loopback): - l = LoopbackIf(loopback['ifname']) - if 'deleted' in loopback.keys(): + l = LoopbackIf(**loopback) + if 'deleted' in loopback: l.remove() else: l.update(loopback) diff --git a/src/conf_mode/interfaces-openvpn.py b/src/conf_mode/interfaces-openvpn.py index 0256ad62a..74e29ed82 100755 --- a/src/conf_mode/interfaces-openvpn.py +++ b/src/conf_mode/interfaces-openvpn.py @@ -17,6 +17,7 @@ import os import re +from cryptography.hazmat.primitives.asymmetric import ec from glob import glob from sys import exit from ipaddress import IPv4Address @@ -31,8 +32,14 @@ from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configverify import verify_vrf from vyos.configverify import verify_bridge_delete -from vyos.configverify import verify_diffie_hellman_length from vyos.ifconfig import VTunIf +from vyos.pki import load_dh_parameters +from vyos.pki import load_private_key +from vyos.pki import wrap_certificate +from vyos.pki import wrap_crl +from vyos.pki import wrap_dh_parameters +from vyos.pki import wrap_openvpn_key +from vyos.pki import wrap_private_key from vyos.template import render from vyos.template import is_ipv4 from vyos.template import is_ipv6 @@ -40,6 +47,7 @@ from vyos.util import call from vyos.util import chown from vyos.util import chmod_600 from vyos.util import dict_search +from vyos.util import dict_search_args from vyos.validate import is_addr_assigned from vyos import ConfigError @@ -49,23 +57,9 @@ airbag.enable() user = 'openvpn' group = 'openvpn' +cfg_dir = '/run/openvpn' cfg_file = '/run/openvpn/{ifname}.conf' -def checkCertHeader(header, filename): - """ - Verify if filename contains specified header. - Returns True if match is found, False if no match or file is not found - """ - if not os.path.isfile(filename): - return False - - with open(filename, 'r') as f: - for line in f: - if re.match(header, line): - return True - - return False - def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -76,14 +70,105 @@ def get_config(config=None): else: conf = Config() base = ['interfaces', 'openvpn'] + + tmp_pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + openvpn = get_interface_dict(conf, base) + if 'deleted' not in openvpn: + openvpn['pki'] = tmp_pki + openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn) openvpn['daemon_user'] = user openvpn['daemon_group'] = group return openvpn +def is_ec_private_key(pki, cert_name): + if not pki or 'certificate' not in pki: + return False + if cert_name not in pki['certificate']: + return False + + pki_cert = pki['certificate'][cert_name] + if 'private' not in pki_cert or 'key' not in pki_cert['private']: + return False + + key = load_private_key(pki_cert['private']['key']) + return isinstance(key, ec.EllipticCurvePrivateKey) + +def verify_pki(openvpn): + pki = openvpn['pki'] + interface = openvpn['ifname'] + mode = openvpn['mode'] + shared_secret_key = dict_search_args(openvpn, 'shared_secret_key') + tls = dict_search_args(openvpn, 'tls') + + if not bool(shared_secret_key) ^ bool(tls): # xor check if only one is set + raise ConfigError('Must specify only one of "shared-secret-key" and "tls"') + + if mode in ['server', 'client'] and not tls: + raise ConfigError('Must specify "tls" for server and client modes') + + if not pki: + raise ConfigError('PKI is not configured') + + if shared_secret_key: + if not dict_search_args(pki, 'openvpn', 'shared_secret'): + raise ConfigError('There are no openvpn shared-secrets in PKI configuration') + + if shared_secret_key not in pki['openvpn']['shared_secret']: + raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}') + + if tls: + if 'ca_certificate' not in tls: + raise ConfigError(f'Must specify "tls ca-certificate" on openvpn interface {interface}') + + if tls['ca_certificate'] not in pki['ca']: + raise ConfigError(f'Invalid CA certificate on openvpn interface {interface}') + + if not (mode == 'client' and 'auth_key' in tls): + if 'certificate' not in tls: + raise ConfigError(f'Missing "tls certificate" on openvpn interface {interface}') + + if 'certificate' in tls: + if tls['certificate'] not in pki['certificate']: + raise ConfigError(f'Invalid certificate on openvpn interface {interface}') + + if dict_search_args(pki, 'certificate', tls['certificate'], 'private', 'password_protected'): + raise ConfigError(f'Cannot use encrypted private key on openvpn interface {interface}') + + if mode == 'server' and 'dh_params' not in tls and not is_ec_private_key(pki, tls['certificate']): + raise ConfigError('Must specify "tls dh-params" when not using EC keys in server mode') + + if 'dh_params' in tls: + if 'dh' not in pki: + raise ConfigError('There are no DH parameters in PKI configuration') + + if tls['dh_params'] not in pki['dh']: + raise ConfigError(f'Invalid dh-params on openvpn interface {interface}') + + pki_dh = pki['dh'][tls['dh_params']] + dh_params = load_dh_parameters(pki_dh['parameters']) + dh_numbers = dh_params.parameter_numbers() + dh_bits = dh_numbers.p.bit_length() + + if dh_bits < 2048: + raise ConfigError(f'Minimum DH key-size is 2048 bits') + + if 'auth_key' in tls or 'crypt_key' in tls: + if not dict_search_args(pki, 'openvpn', 'shared_secret'): + raise ConfigError('There are no openvpn shared-secrets in PKI configuration') + + if 'auth_key' in tls: + if tls['auth_key'] not in pki['openvpn']['shared_secret']: + raise ConfigError(f'Invalid auth-key on openvpn interface {interface}') + + if 'crypt_key' in tls: + if tls['crypt_key'] not in pki['openvpn']['shared_secret']: + raise ConfigError(f'Invalid crypt-key on openvpn interface {interface}') + def verify(openvpn): if 'deleted' in openvpn: verify_bridge_delete(openvpn) @@ -108,8 +193,8 @@ def verify(openvpn): if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Protocol "tcp-passive" is not valid in client mode') - if dict_search('tls.dh_file', openvpn): - raise ConfigError('Cannot specify "tls dh-file" in client mode') + if dict_search('tls.dh_params', openvpn): + raise ConfigError('Cannot specify "tls dh-params" in client mode') # # OpenVPN site-to-site - VERIFY @@ -194,11 +279,6 @@ def verify(openvpn): if 'remote_host' in openvpn: raise ConfigError('Cannot specify "remote-host" in server mode') - if 'tls' in openvpn: - if 'dh_file' not in openvpn['tls']: - if 'key_file' in openvpn['tls'] and not checkCertHeader('-----BEGIN EC PRIVATE KEY-----', openvpn['tls']['key_file']): - raise ConfigError('Must specify "tls dh-file" when not using EC keys in server mode') - tmp = dict_search('server.subnet', openvpn) if tmp: v4_subnets = len([subnet for subnet in tmp if is_ipv4(subnet)]) @@ -306,97 +386,40 @@ def verify(openvpn): if 'remote_host' not in openvpn: raise ConfigError('Must specify "remote-host" with "tcp-active"') - # shared secret and TLS - if not ('shared_secret_key_file' in openvpn or 'tls' in openvpn): - raise ConfigError('Must specify one of "shared-secret-key-file" and "tls"') - - if {'shared_secret_key_file', 'tls'} <= set(openvpn): - raise ConfigError('Can only specify one of "shared-secret-key-file" and "tls"') - - if openvpn['mode'] in ['client', 'server']: - if 'tls' not in openvpn: - raise ConfigError('Must specify "tls" for server and client mode') - # # TLS/encryption # - if 'shared_secret_key_file' in openvpn: + if 'shared_secret_key' in openvpn: if dict_search('encryption.cipher', openvpn) in ['aes128gcm', 'aes192gcm', 'aes256gcm']: - raise ConfigError('GCM encryption with shared-secret-key-file not supported') - - file = dict_search('shared_secret_key_file', openvpn) - if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): - raise ConfigError(f'Specified shared-secret-key-file "{file}" is not valid') + raise ConfigError('GCM encryption with shared-secret-key not supported') if 'tls' in openvpn: - if 'ca_cert_file' not in openvpn['tls']: - raise ConfigError('Must specify "tls ca-cert-file"') - - if not (openvpn['mode'] == 'client' and 'auth_file' in openvpn['tls']): - if 'cert_file' not in openvpn['tls']: - raise ConfigError('Missing "tls cert-file"') - - if 'key_file' not in openvpn['tls']: - raise ConfigError('Missing "tls key-file"') - - if {'auth_file', 'crypt_file'} <= set(openvpn['tls']): - raise ConfigError('TLS auth and crypt are mutually exclusive') - - file = dict_search('tls.ca_cert_file', openvpn) - if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file): - raise ConfigError(f'Specified ca-cert-file "{file}" is invalid') - - file = dict_search('tls.auth_file', openvpn) - if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): - raise ConfigError(f'Specified auth-file "{file}" is invalid') - - file = dict_search('tls.cert_file', openvpn) - if file and not checkCertHeader('-----BEGIN CERTIFICATE-----', file): - raise ConfigError(f'Specified cert-file "{file}" is invalid') - - file = dict_search('tls.key_file', openvpn) - if file and not checkCertHeader('-----BEGIN (?:RSA |EC )?PRIVATE KEY-----', file): - raise ConfigError(f'Specified key-file "{file}" is not valid') - - file = dict_search('tls.crypt_file', openvpn) - if file and not checkCertHeader('-----BEGIN OpenVPN Static key V1-----', file): - raise ConfigError(f'Specified TLS crypt-file "{file}" is invalid') - - file = dict_search('tls.crl_file', openvpn) - if file and not checkCertHeader('-----BEGIN X509 CRL-----', file): - raise ConfigError(f'Specified crl-file "{file} not valid') - - file = dict_search('tls.dh_file', openvpn) - if file and not checkCertHeader('-----BEGIN DH PARAMETERS-----', file): - raise ConfigError(f'Specified dh-file "{file}" is not valid') - - if file and not verify_diffie_hellman_length(file, 2048): - raise ConfigError(f'Minimum DH key-size is 2048 bits') + if {'auth_key', 'crypt_key'} <= set(openvpn['tls']): + raise ConfigError('TLS auth and crypt keys are mutually exclusive') tmp = dict_search('tls.role', openvpn) if tmp: if openvpn['mode'] in ['client', 'server']: - if not dict_search('tls.auth_file', openvpn): + if not dict_search('tls.auth_key', openvpn): raise ConfigError('Cannot specify "tls role" in client-server mode') if tmp == 'active': if openvpn['protocol'] == 'tcp-passive': raise ConfigError('Cannot specify "tcp-passive" when "tls role" is "active"') - if dict_search('tls.dh_file', openvpn): - raise ConfigError('Cannot specify "tls dh-file" when "tls role" is "active"') + if dict_search('tls.dh_params', openvpn): + raise ConfigError('Cannot specify "tls dh-params" when "tls role" is "active"') elif tmp == 'passive': if openvpn['protocol'] == 'tcp-active': raise ConfigError('Cannot specify "tcp-active" when "tls role" is "passive"') - if not dict_search('tls.dh_file', openvpn): - raise ConfigError('Must specify "tls dh-file" when "tls role" is "passive"') + if not dict_search('tls.dh_params', openvpn): + raise ConfigError('Must specify "tls dh-params" when "tls role" is "passive"') - file = dict_search('tls.key_file', openvpn) - if file and checkCertHeader('-----BEGIN EC PRIVATE KEY-----', file): - if dict_search('tls.dh_file', openvpn): - print('Warning: using dh-file and EC keys simultaneously will ' \ + if 'certificate' in openvpn['tls'] and is_ec_private_key(openvpn['pki'], openvpn['tls']['certificate']): + if 'dh_params' in openvpn['tls']: + print('Warning: using dh-params and EC keys simultaneously will ' \ 'lead to DH ciphers being used instead of ECDH') if dict_search('encryption.cipher', openvpn) == 'none': @@ -404,6 +427,8 @@ def verify(openvpn): print('No encryption will be performed and data is transmitted in ' \ 'plain text over the network!') + verify_pki(openvpn) + # # Auth user/pass # @@ -419,6 +444,110 @@ def verify(openvpn): return None +def generate_pki_files(openvpn): + pki = openvpn['pki'] + + if not pki: + return None + + interface = openvpn['ifname'] + shared_secret_key = dict_search_args(openvpn, 'shared_secret_key') + tls = dict_search_args(openvpn, 'tls') + + files = [] + + if shared_secret_key: + pki_key = pki['openvpn']['shared_secret'][shared_secret_key] + key_path = os.path.join(cfg_dir, f'{interface}_shared.key') + + with open(key_path, 'w') as f: + f.write(wrap_openvpn_key(pki_key['key'])) + + files.append(key_path) + + if tls: + if 'ca_certificate' in tls: + cert_name = tls['ca_certificate'] + pki_ca = pki['ca'][cert_name] + + if 'certificate' in pki_ca: + cert_path = os.path.join(cfg_dir, f'{interface}_ca.pem') + + with open(cert_path, 'w') as f: + f.write(wrap_certificate(pki_ca['certificate'])) + + files.append(cert_path) + + if 'crl' in pki_ca: + for crl in pki_ca['crl']: + crl_path = os.path.join(cfg_dir, f'{interface}_crl.pem') + + with open(crl_path, 'w') as f: + f.write(wrap_crl(crl)) + + files.append(crl_path) + openvpn['tls']['crl'] = True + + if 'certificate' in tls: + cert_name = tls['certificate'] + pki_cert = pki['certificate'][cert_name] + + if 'certificate' in pki_cert: + cert_path = os.path.join(cfg_dir, f'{interface}_cert.pem') + + with open(cert_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + files.append(cert_path) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + key_path = os.path.join(cfg_dir, f'{interface}_cert.key') + + with open(key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + files.append(key_path) + openvpn['tls']['private_key'] = True + + if 'dh_params' in tls: + dh_name = tls['dh_params'] + pki_dh = pki['dh'][dh_name] + + if 'parameters' in pki_dh: + dh_path = os.path.join(cfg_dir, f'{interface}_dh.pem') + + with open(dh_path, 'w') as f: + f.write(wrap_dh_parameters(pki_dh['parameters'])) + + files.append(dh_path) + + if 'auth_key' in tls: + key_name = tls['auth_key'] + pki_key = pki['openvpn']['shared_secret'][key_name] + + if 'key' in pki_key: + key_path = os.path.join(cfg_dir, f'{interface}_auth.key') + + with open(key_path, 'w') as f: + f.write(wrap_openvpn_key(pki_key['key'])) + + files.append(key_path) + + if 'crypt_key' in tls: + key_name = tls['crypt_key'] + pki_key = pki['openvpn']['shared_secret'][key_name] + + if 'key' in pki_key: + key_path = os.path.join(cfg_dir, f'{interface}_crypt.key') + + with open(key_path, 'w') as f: + f.write(wrap_openvpn_key(pki_key['key'])) + + files.append(key_path) + + return files + + def generate(openvpn): interface = openvpn['ifname'] directory = os.path.dirname(cfg_file.format(**openvpn)) @@ -438,13 +567,7 @@ def generate(openvpn): chown(ccd_dir, user, group) # Fix file permissons for keys - fix_permissions = [] - - tmp = dict_search('shared_secret_key_file', openvpn) - if tmp: fix_permissions.append(openvpn['shared_secret_key_file']) - - tmp = dict_search('tls.key_file', openvpn) - if tmp: fix_permissions.append(tmp) + fix_permissions = generate_pki_files(openvpn) # Generate User/Password authentication file if 'authentication' in openvpn: @@ -456,8 +579,9 @@ def generate(openvpn): os.remove(openvpn['auth_user_pass_file']) # Generate client specific configuration - if dict_search('server.client', openvpn): - for client, client_config in dict_search('server.client', openvpn).items(): + server_client = dict_search_args(openvpn, 'server', 'client') + if server_client: + for client, client_config in server_client.items(): client_file = os.path.join(ccd_dir, client) # Our client need's to know its subnet mask ... diff --git a/src/conf_mode/interfaces-vti.py b/src/conf_mode/interfaces-vti.py index 1b38304c1..57950ffea 100755 --- a/src/conf_mode/interfaces-vti.py +++ b/src/conf_mode/interfaces-vti.py @@ -45,13 +45,13 @@ def generate(vti): return None def apply(vti): - if vti['ifname'] in interfaces(): - # Always delete the VTI interface in advance + # Remove macsec interface + if 'deleted' in vti: VTIIf(**vti).remove() + return None - if 'deleted' not in vti: - tmp = VTIIf(**vti) - tmp.update(vti) + tmp = VTIIf(**vti) + tmp.update(vti) return None diff --git a/src/conf_mode/interfaces-vxlan.py b/src/conf_mode/interfaces-vxlan.py index 8e6247a30..804f2d14f 100755 --- a/src/conf_mode/interfaces-vxlan.py +++ b/src/conf_mode/interfaces-vxlan.py @@ -25,7 +25,9 @@ from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete from vyos.configverify import verify_mtu_ipv6 from vyos.configverify import verify_source_interface -from vyos.ifconfig import VXLANIf, Interface +from vyos.ifconfig import Interface +from vyos.ifconfig import VXLANIf +from vyos.template import is_ipv6 from vyos import ConfigError from vyos import airbag airbag.enable() @@ -65,12 +67,19 @@ def verify(vxlan): raise ConfigError('Must configure VNI for VXLAN') if 'source_interface' in vxlan: - # VXLAN adds a 50 byte overhead - we need to check the underlaying MTU - # if our configured MTU is at least 50 bytes less + # VXLAN adds at least an overhead of 50 byte - we need to check the + # underlaying device if our VXLAN package is not going to be fragmented! + vxlan_overhead = 50 + if 'source_address' in vxlan and is_ipv6(vxlan['source_address']): + # IPv6 adds an extra 20 bytes overhead because the IPv6 header is 20 + # bytes larger than the IPv4 header - assuming no extra options are + # in use. + vxlan_overhead += 20 + lower_mtu = Interface(vxlan['source_interface']).get_mtu() - if lower_mtu < (int(vxlan['mtu']) + 50): - raise ConfigError('VXLAN has a 50 byte overhead, underlaying device ' \ - f'MTU is to small ({lower_mtu} bytes)') + if lower_mtu < (int(vxlan['mtu']) + vxlan_overhead): + raise ConfigError(f'Underlaying device MTU is to small ({lower_mtu} '\ + f'bytes) for VXLAN overhead ({vxlan_overhead} bytes!)') verify_mtu_ipv6(vxlan) verify_address(vxlan) diff --git a/src/conf_mode/interfaces-wireguard.py b/src/conf_mode/interfaces-wireguard.py index 024ab8f59..4c566a5ad 100755 --- a/src/conf_mode/interfaces-wireguard.py +++ b/src/conf_mode/interfaces-wireguard.py @@ -46,17 +46,14 @@ def get_config(config=None): base = ['interfaces', 'wireguard'] wireguard = get_interface_dict(conf, base) - # Mangle private key - it has a default so its always valid - wireguard['private_key'] = '/config/auth/wireguard/{private_key}/private.key'.format(**wireguard) - # Determine which Wireguard peer has been removed. # Peers can only be removed with their public key! dict = {} tmp = node_changed(conf, ['peer'], key_mangling=('-', '_')) for peer in (tmp or []): - pubkey = leaf_node_changed(conf, ['peer', peer, 'pubkey']) - if pubkey: - dict = dict_merge({'peer_remove' : {peer : {'pubkey' : pubkey[0]}}}, dict) + public_key = leaf_node_changed(conf, ['peer', peer, 'public_key']) + if public_key: + dict = dict_merge({'peer_remove' : {peer : {'public_key' : public_key[0]}}}, dict) wireguard.update(dict) return wireguard @@ -70,9 +67,8 @@ def verify(wireguard): verify_address(wireguard) verify_vrf(wireguard) - if not os.path.exists(wireguard['private_key']): - raise ConfigError('Wireguard private-key not found! Execute: ' \ - '"run generate wireguard [default-keypair|named-keypairs]"') + if 'private_key' not in wireguard: + raise ConfigError('Wireguard private-key not defined') if 'peer' not in wireguard: raise ConfigError('At least one Wireguard peer is required!') @@ -84,7 +80,7 @@ def verify(wireguard): if 'allowed_ips' not in peer: raise ConfigError(f'Wireguard allowed-ips required for peer "{tmp}"!') - if 'pubkey' not in peer: + if 'public_key' not in peer: raise ConfigError(f'Wireguard public-key required for peer "{tmp}"!') if ('address' in peer and 'port' not in peer) or ('port' in peer and 'address' not in peer): diff --git a/src/conf_mode/ipsec-settings.py b/src/conf_mode/ipsec-settings.py deleted file mode 100755 index a373f821f..000000000 --- a/src/conf_mode/ipsec-settings.py +++ /dev/null @@ -1,223 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import re -import os - -from time import sleep -from sys import exit - -from vyos.config import Config -from vyos import ConfigError -from vyos.util import call -from vyos.template import render - -from vyos import airbag -airbag.enable() - -ra_conn_name = "remote-access" -ipsec_secrets_file = "/etc/ipsec.secrets" -ipsec_ra_conn_dir = "/etc/ipsec.d/tunnels/" -ipsec_ra_conn_file = ipsec_ra_conn_dir + ra_conn_name -ipsec_conf_file = "/etc/ipsec.conf" -ca_cert_path = "/etc/ipsec.d/cacerts" -server_cert_path = "/etc/ipsec.d/certs" -server_key_path = "/etc/ipsec.d/private" -delim_ipsec_l2tp_begin = "### VyOS L2TP VPN Begin ###" -delim_ipsec_l2tp_end = "### VyOS L2TP VPN End ###" -charon_pidfile = "/var/run/charon.pid" - -def get_config(config=None): - if config: - config = config - else: - config = Config() - - if config.exists("vpn ipsec ipsec-interfaces interface"): - data["ipsec_interfaces"] = config.return_values("vpn ipsec ipsec-interfaces interface") - - # Init config variables - data["delim_ipsec_l2tp_begin"] = delim_ipsec_l2tp_begin - data["delim_ipsec_l2tp_end"] = delim_ipsec_l2tp_end - data["ipsec_ra_conn_file"] = ipsec_ra_conn_file - data["ra_conn_name"] = ra_conn_name - # Get l2tp ipsec settings - data["ipsec_l2tp"] = False - conf_ipsec_command = "vpn l2tp remote-access ipsec-settings " #last space is useful - if config.exists(conf_ipsec_command): - data["ipsec_l2tp"] = True - - # Authentication params - if config.exists(conf_ipsec_command + "authentication mode"): - data["ipsec_l2tp_auth_mode"] = config.return_value(conf_ipsec_command + "authentication mode") - if config.exists(conf_ipsec_command + "authentication pre-shared-secret"): - data["ipsec_l2tp_secret"] = config.return_value(conf_ipsec_command + "authentication pre-shared-secret") - - # mode x509 - if config.exists(conf_ipsec_command + "authentication x509 ca-cert-file"): - data["ipsec_l2tp_x509_ca_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 ca-cert-file") - if config.exists(conf_ipsec_command + "authentication x509 crl-file"): - data["ipsec_l2tp_x509_crl_file"] = config.return_value(conf_ipsec_command + "authentication x509 crl-file") - if config.exists(conf_ipsec_command + "authentication x509 server-cert-file"): - data["ipsec_l2tp_x509_server_cert_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-cert-file") - data["server_cert_file_copied"] = server_cert_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-cert-file")).group(0) - if config.exists(conf_ipsec_command + "authentication x509 server-key-file"): - data["ipsec_l2tp_x509_server_key_file"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-file") - data["server_key_file_copied"] = server_key_path+"/"+re.search('\w+(?:\.\w+)*$', config.return_value(conf_ipsec_command + "authentication x509 server-key-file")).group(0) - if config.exists(conf_ipsec_command + "authentication x509 server-key-password"): - data["ipsec_l2tp_x509_server_key_password"] = config.return_value(conf_ipsec_command + "authentication x509 server-key-password") - - # Common l2tp ipsec params - if config.exists(conf_ipsec_command + "ike-lifetime"): - data["ipsec_l2tp_ike_lifetime"] = config.return_value(conf_ipsec_command + "ike-lifetime") - else: - data["ipsec_l2tp_ike_lifetime"] = "3600" - - if config.exists(conf_ipsec_command + "lifetime"): - data["ipsec_l2tp_lifetime"] = config.return_value(conf_ipsec_command + "lifetime") - else: - data["ipsec_l2tp_lifetime"] = "3600" - - if config.exists("vpn l2tp remote-access outside-address"): - data['outside_addr'] = config.return_value('vpn l2tp remote-access outside-address') - - return data - -def write_ipsec_secrets(c): - if c.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": - secret_txt = "{0}\n{1} %any : PSK \"{2}\"\n{3}\n".format(delim_ipsec_l2tp_begin, c['outside_addr'], c['ipsec_l2tp_secret'], delim_ipsec_l2tp_end) - elif c.get("ipsec_l2tp_auth_mode") == "x509": - secret_txt = "{0}\n: RSA {1}\n{2}\n".format(delim_ipsec_l2tp_begin, c['server_key_file_copied'], delim_ipsec_l2tp_end) - - old_umask = os.umask(0o077) - with open(ipsec_secrets_file, 'a+') as f: - f.write(secret_txt) - os.umask(old_umask) - -def write_ipsec_conf(c): - ipsec_confg_txt = "{0}\ninclude {1}\n{2}\n".format(delim_ipsec_l2tp_begin, ipsec_ra_conn_file, delim_ipsec_l2tp_end) - - old_umask = os.umask(0o077) - with open(ipsec_conf_file, 'a+') as f: - f.write(ipsec_confg_txt) - os.umask(old_umask) - -### Remove config from file by delimiter -def remove_confs(delim_begin, delim_end, conf_file): - call("sed -i '/"+delim_begin+"/,/"+delim_end+"/d' "+conf_file) - - -### Checking certificate storage and notice if certificate not in /config directory -def check_cert_file_store(cert_name, file_path, dts_path): - if not re.search('^\/config\/.+', file_path): - print("Warning: \"" + file_path + "\" lies outside of /config/auth directory. It will not get preserved during image upgrade.") - #Checking file existence - if not os.path.isfile(file_path): - raise ConfigError("L2TP VPN configuration error: Invalid "+cert_name+" \""+file_path+"\"") - else: - ### Cpy file to /etc/ipsec.d/certs/ /etc/ipsec.d/cacerts/ - # todo make check - ret = call('cp -f '+file_path+' '+dts_path) - if ret: - raise ConfigError("L2TP VPN configuration error: Cannot copy "+file_path) - -def verify(data): - # l2tp ipsec check - if data["ipsec_l2tp"]: - # Checking dependecies for "authentication mode pre-shared-secret" - if data.get("ipsec_l2tp_auth_mode") == "pre-shared-secret": - if not data.get("ipsec_l2tp_secret"): - raise ConfigError("pre-shared-secret required") - if not data.get("outside_addr"): - raise ConfigError("outside-address not defined") - - # Checking dependecies for "authentication mode x509" - if data.get("ipsec_l2tp_auth_mode") == "x509": - if not data.get("ipsec_l2tp_x509_server_key_file"): - raise ConfigError("L2TP VPN configuration error: \"server-key-file\" not defined.") - else: - check_cert_file_store("server-key-file", data['ipsec_l2tp_x509_server_key_file'], server_key_path) - - if not data.get("ipsec_l2tp_x509_server_cert_file"): - raise ConfigError("L2TP VPN configuration error: \"server-cert-file\" not defined.") - else: - check_cert_file_store("server-cert-file", data['ipsec_l2tp_x509_server_cert_file'], server_cert_path) - - if not data.get("ipsec_l2tp_x509_ca_cert_file"): - raise ConfigError("L2TP VPN configuration error: \"ca-cert-file\" must be defined for X.509") - else: - check_cert_file_store("ca-cert-file", data['ipsec_l2tp_x509_ca_cert_file'], ca_cert_path) - - if not data.get('ipsec_interfaces'): - raise ConfigError("L2TP VPN configuration error: \"vpn ipsec ipsec-interfaces\" must be specified.") - -def generate(data): - if data["ipsec_l2tp"]: - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) - # old_umask = os.umask(0o077) - # render(ipsec_secrets_file, 'ipsec/ipsec.secrets.tmpl', data) - # os.umask(old_umask) - ## Use this method while IPSec CLI handler won't be overwritten to python - write_ipsec_secrets(data) - - old_umask = os.umask(0o077) - - # Create tunnels directory if does not exist - if not os.path.exists(ipsec_ra_conn_dir): - os.makedirs(ipsec_ra_conn_dir) - - render(ipsec_ra_conn_file, 'ipsec/remote-access.tmpl', data) - os.umask(old_umask) - - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) - # old_umask = os.umask(0o077) - # render(ipsec_conf_file, 'ipsec/ipsec.conf.tmpl', data) - # os.umask(old_umask) - ## Use this method while IPSec CLI handler won't be overwritten to python - write_ipsec_conf(data) - - else: - if os.path.exists(ipsec_ra_conn_file): - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_ra_conn_file) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_secrets_file) - remove_confs(delim_ipsec_l2tp_begin, delim_ipsec_l2tp_end, ipsec_conf_file) - -def restart_ipsec(): - call('ipsec restart >&/dev/null') - # counter for apply swanctl config - counter = 10 - while counter <= 10: - if os.path.exists(charon_pidfile): - call('swanctl -q >&/dev/null') - break - counter -=1 - sleep(1) - if counter == 0: - raise ConfigError('VPN configuration error: IPSec is not running.') - -def apply(data): - # Restart IPSec daemon - restart_ipsec() - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/le_cert.py b/src/conf_mode/le_cert.py index 755c89966..6e169a3d5 100755 --- a/src/conf_mode/le_cert.py +++ b/src/conf_mode/le_cert.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos import ConfigError from vyos.util import cmd from vyos.util import call +from vyos.util import is_systemd_service_running from vyos import airbag airbag.enable() @@ -87,8 +88,7 @@ def generate(cert): # certbot will attempt to reload nginx, even with 'certonly'; # start nginx if not active - ret = call('systemctl is-active --quiet nginx.service') - if ret: + if not is_systemd_service_running('nginx.service'): call('systemctl start nginx.service') request_certbot(cert) diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index dd70d6bab..348bae59f 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -102,12 +102,6 @@ def apply(bfd): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bfd['new_frr_config']) frr_cfg.commit_configuration() - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if bfd['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration() - return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index 95f277d74..9ecfd07fe 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -57,6 +57,11 @@ def get_config(config=None): if not conf.exists(base): bgp.update({'deleted' : ''}) + if not vrf: + # We are running in the default VRF context, thus we can not delete + # our main BGP instance if there are dependent BGP VRF instances. + bgp['dependent_vrfs'] = conf.get_config_dict(['vrf', 'name'], + key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return bgp # We also need some additional information from the config, prefix-lists @@ -96,6 +101,11 @@ def verify_remote_as(peer_config, bgp_config): def verify(bgp): if not bgp or 'deleted' in bgp: + if 'dependent_vrfs' in bgp: + for vrf, vrf_options in bgp['dependent_vrfs'].items(): + if dict_search('protocols.bgp', vrf_options) != None: + raise ConfigError('Cannot delete default BGP instance, ' \ + 'dependent VRF instance(s) exist!') return None if 'local_as' not in bgp: @@ -271,15 +281,6 @@ def apply(bgp): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', bgp['frr_bgpd_config']) frr_cfg.commit_configuration(bgp_daemon) - # If FRR config is blank, re-run the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if bgp['frr_bgpd_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(bgp_daemon) - if bgp['frr_zebra_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(zebra_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index c3a444f16..4cf0312e9 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -113,9 +113,13 @@ def verify(isis): # Interface MTU must be >= configured lsp-mtu mtu = Interface(interface).get_mtu() area_mtu = isis['lsp_mtu'] - if mtu < int(area_mtu): - raise ConfigError(f'Interface {interface} has MTU {mtu}, minimum ' \ - f'area MTU is {area_mtu}!') + # Recommended maximum PDU size = interface MTU - 3 bytes + recom_area_mtu = mtu - 3 + if mtu < int(area_mtu) or int(area_mtu) > recom_area_mtu: + raise ConfigError(f'Interface {interface} has MTU {mtu}, ' \ + f'current area MTU is {area_mtu}! \n' \ + f'Recommended area lsp-mtu {recom_area_mtu} or less ' \ + '(calculated on MTU size).') if 'vrf' in isis: # If interface specific options are set, we must ensure that the @@ -149,7 +153,7 @@ def verify(isis): # If Redistribute set, but level don't set if 'redistribute' in isis: proc_level = isis.get('level','').replace('-','_') - for afi in ['ipv4']: + for afi in ['ipv4', 'ipv6']: if afi not in isis['redistribute']: continue @@ -198,7 +202,7 @@ def generate(isis): isis['protocol'] = 'isis' # required for frr/vrf.route-map.frr.tmpl isis['frr_zebra_config'] = render_to_string('frr/vrf.route-map.frr.tmpl', isis) - isis['frr_isisd_config'] = render_to_string('frr/isis.frr.tmpl', isis) + isis['frr_isisd_config'] = render_to_string('frr/isisd.frr.tmpl', isis) return None def apply(isis): @@ -232,15 +236,6 @@ def apply(isis): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', isis['frr_isisd_config']) frr_cfg.commit_configuration(isis_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if isis['frr_isisd_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(isis_daemon) - if isis['frr_zebra_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(zebra_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 21eb8e447..78c1c82bd 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -211,15 +211,6 @@ def apply(ospf): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospf['frr_ospfd_config']) frr_cfg.commit_configuration(ospf_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if ospf['frr_ospfd_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(ospf_daemon) - if ospf['frr_zebra_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(zebra_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_ospfv3.py b/src/conf_mode/protocols_ospfv3.py index 1964e9d34..fef0f509b 100755 --- a/src/conf_mode/protocols_ospfv3.py +++ b/src/conf_mode/protocols_ospfv3.py @@ -86,12 +86,6 @@ def apply(ospfv3): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ospfv3['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) - # If FRR config is blank, re-run the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if ospfv3['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(frr_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_rip.py b/src/conf_mode/protocols_rip.py index 907ac54ac..e56eb1f56 100755 --- a/src/conf_mode/protocols_rip.py +++ b/src/conf_mode/protocols_rip.py @@ -117,12 +117,6 @@ def apply(rip): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rip['new_frr_config']) frr_cfg.commit_configuration(rip_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if rip['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(rip_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_ripng.py b/src/conf_mode/protocols_ripng.py index 44c080546..aaec5dacb 100755 --- a/src/conf_mode/protocols_ripng.py +++ b/src/conf_mode/protocols_ripng.py @@ -108,12 +108,6 @@ def apply(ripng): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', ripng['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if ripng['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(frr_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/protocols_rpki.py b/src/conf_mode/protocols_rpki.py index d8f99efb8..947c8ab7a 100755 --- a/src/conf_mode/protocols_rpki.py +++ b/src/conf_mode/protocols_rpki.py @@ -90,12 +90,6 @@ def apply(rpki): frr_cfg.add_before(r'(ip prefix-list .*|route-map .*|line vty)', rpki['new_frr_config']) frr_cfg.commit_configuration(frr_daemon) - # If FRR config is blank, re-run the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if rpki['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(frr_daemon) - return None if __name__ == '__main__': diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 1d45cb71c..338247e30 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -107,12 +107,6 @@ def apply(static): frr_cfg.add_before(r'(interface .*|line vty)', static['new_frr_config']) frr_cfg.commit_configuration(static_daemon) - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if static['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(static_daemon) - # Save configuration to /run/frr/config/frr.conf frr.save_configuration() diff --git a/src/conf_mode/snmp.py b/src/conf_mode/snmp.py index 3990e5735..23e45a5b7 100755 --- a/src/conf_mode/snmp.py +++ b/src/conf_mode/snmp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -54,6 +54,7 @@ default_config_data = { 'location' : '', 'description' : '', 'contact' : '', + 'route_table': 'False', 'trap_source': '', 'trap_targets': [], 'vyos_user': '', @@ -186,6 +187,9 @@ def get_config(): snmp['script_ext'].append(extension) + if conf.exists('oid-enable route-table'): + snmp['route_table'] = True + if conf.exists('vrf'): # Append key to dict but don't place it in the default dictionary. # This is required to make the override.conf.tmpl work until we diff --git a/src/conf_mode/system-login.py b/src/conf_mode/system-login.py index da0fc2a25..f0b92aea8 100755 --- a/src/conf_mode/system-login.py +++ b/src/conf_mode/system-login.py @@ -43,12 +43,11 @@ radius_config_file = "/etc/pam_radius_auth.conf" def get_local_users(): """Return list of dynamically allocated users (see Debian Policy Manual)""" local_users = [] - for p in getpwall(): - username = p[0] - uid = getpwnam(username).pw_uid + for s_user in getpwall(): + uid = getpwnam(s_user.pw_name).pw_uid if uid in range(1000, 29999): - if username not in ['radius_user', 'radius_priv_user']: - local_users.append(username) + if s_user.pw_name not in ['radius_user', 'radius_priv_user']: + local_users.append(s_user.pw_name) return local_users @@ -104,7 +103,14 @@ def verify(login): raise ConfigError(f'Attempting to delete current user: {cur_user}') if 'user' in login: + system_users = getpwall() for user, user_config in login['user'].items(): + # Linux system users range up until UID 1000, we can not create a + # VyOS CLI user which already exists as system user + for s_user in system_users: + if s_user.pw_name == user and s_user.pw_uid < 1000: + raise ConfigError(f'User "{user}" can not be created, conflict with local system account!') + for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items(): if 'type' not in pubkey_options: raise ConfigError(f'Missing type for public-key "{pubkey}"!') diff --git a/src/conf_mode/system-option.py b/src/conf_mode/system-option.py index 454611c55..55cf6b142 100755 --- a/src/conf_mode/system-option.py +++ b/src/conf_mode/system-option.py @@ -24,6 +24,7 @@ from vyos.config import Config from vyos.configdict import dict_merge from vyos.template import render from vyos.util import cmd +from vyos.util import is_systemd_service_running from vyos.validate import is_addr_assigned from vyos.xml import defaults from vyos import ConfigError @@ -114,7 +115,7 @@ def apply(options): if 'performance' in options: cmd('systemctl restart tuned.service') # wait until daemon has started before sending configuration - while (int(os.system('systemctl is-active --quiet tuned.service')) != 0): + while (not is_systemd_service_running('tuned.service')): sleep(0.250) cmd('tuned-adm profile network-{performance}'.format(**options)) else: diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 3fab8e868..d3065fc47 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -33,9 +33,12 @@ from vyos.pki import wrap_crl from vyos.pki import wrap_public_key from vyos.pki import wrap_private_key from vyos.template import ip_from_cidr +from vyos.template import is_ipv4 +from vyos.template import is_ipv6 from vyos.template import render from vyos.validate import is_ipv6_link_local from vyos.util import call +from vyos.util import dict_search from vyos.util import dict_search_args from vyos.util import run from vyos.xml import defaults @@ -46,13 +49,14 @@ airbag.enable() dhcp_wait_attempts = 2 dhcp_wait_sleep = 1 -swanctl_dir = '/etc/swanctl' -ipsec_conf = '/etc/ipsec.conf' -ipsec_secrets = '/etc/ipsec.secrets' -charon_conf = '/etc/strongswan.d/charon.conf' -charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' -interface_conf = '/etc/strongswan.d/interfaces_use.conf' -swanctl_conf = f'{swanctl_dir}/swanctl.conf' +swanctl_dir = '/etc/swanctl' +ipsec_conf = '/etc/ipsec.conf' +ipsec_secrets = '/etc/ipsec.secrets' +charon_conf = '/etc/strongswan.d/charon.conf' +charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf' +charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf' +interface_conf = '/etc/strongswan.d/interfaces_use.conf' +swanctl_conf = f'{swanctl_dir}/swanctl.conf' default_install_routes = 'yes' @@ -73,6 +77,7 @@ def get_config(config=None): else: conf = Config() base = ['vpn', 'ipsec'] + l2tp_base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] if not conf.exists(base): return None @@ -97,26 +102,51 @@ def get_config(config=None): ipsec['esp_group'][group]) if 'ike_group' in ipsec: default_values = defaults(base + ['ike-group']) + # proposal is a tag node which may come with individual defaults per node + if 'proposal' in default_values: + del default_values['proposal'] + for group in ipsec['ike_group']: ipsec['ike_group'][group] = dict_merge(default_values, ipsec['ike_group'][group]) - if 'remote_access' in ipsec: - default_values = defaults(base + ['remote-access']) - for rw in ipsec['remote_access']: - ipsec['remote_access'][rw] = dict_merge(default_values, - ipsec['remote_access'][rw]) + + if 'proposal' in ipsec['ike_group'][group]: + default_values = defaults(base + ['ike-group', 'proposal']) + for proposal in ipsec['ike_group'][group]['proposal']: + ipsec['ike_group'][group]['proposal'][proposal] = dict_merge(default_values, + ipsec['ike_group'][group]['proposal'][proposal]) + + if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']: + default_values = defaults(base + ['remote-access', 'connection']) + for rw in ipsec['remote_access']['connection']: + ipsec['remote_access']['connection'][rw] = dict_merge(default_values, + ipsec['remote_access']['connection'][rw]) + + if 'remote_access' in ipsec and 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: + default_values = defaults(base + ['remote-access', 'radius', 'server']) + for server in ipsec['remote_access']['radius']['server']: + ipsec['remote_access']['radius']['server'][server] = dict_merge(default_values, + ipsec['remote_access']['radius']['server'][server]) ipsec['dhcp_no_address'] = {} ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes - ipsec['interface_change'] = leaf_node_changed(conf, base + ['ipsec-interfaces', - 'interface']) - ipsec['l2tp_exists'] = conf.exists(['vpn', 'l2tp', 'remote-access', - 'ipsec-settings']) + ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface']) ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel']) ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + if tmp: + ipsec['l2tp'] = tmp + l2tp_defaults = defaults(l2tp_base) + ipsec['l2tp'] = dict_merge(l2tp_defaults, ipsec['l2tp']) + ipsec['l2tp_outside_address'] = conf.return_value(['vpn', 'l2tp', 'remote-access', 'outside-address']) + ipsec['l2tp_ike_default'] = 'aes256-sha1-modp1024,3des-sha1-modp1024' + ipsec['l2tp_esp_default'] = 'aes256-sha1,3des-sha1' + return ipsec def get_dhcp_address(iface): @@ -165,14 +195,43 @@ def verify(ipsec): if not ipsec: return None - if 'ipsec_interfaces' in ipsec and 'interface' in ipsec['ipsec_interfaces']: - interfaces = ipsec['ipsec_interfaces']['interface'] - if isinstance(interfaces, str): - interfaces = [interfaces] - - for ifname in interfaces: + if 'interfaces' in ipsec : + for ifname in ipsec['interface']: verify_interface_exists(ifname) + if 'l2tp' in ipsec: + if 'esp_group' in ipsec['l2tp']: + if 'esp_group' not in ipsec or ipsec['l2tp']['esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on L2TP remote-access config") + + if 'ike_group' in ipsec['l2tp']: + if 'ike_group' not in ipsec or ipsec['l2tp']['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on L2TP remote-access config") + + if 'authentication' not in ipsec['l2tp']: + raise ConfigError(f'Missing authentication settings on L2TP remote-access config') + + if 'mode' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing authentication mode on L2TP remote-access config') + + if not ipsec['l2tp_outside_address']: + raise ConfigError(f'Missing outside-address on L2TP remote-access config') + + if ipsec['l2tp']['authentication']['mode'] == 'pre-shared-secret': + if 'pre_shared_secret' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing pre shared secret on L2TP remote-access config') + + if ipsec['l2tp']['authentication']['mode'] == 'x509': + if 'x509' not in ipsec['l2tp']['authentication']: + raise ConfigError(f'Missing x509 settings on L2TP remote-access config') + + x509 = ipsec['l2tp']['authentication']['x509'] + + if 'ca_certificate' not in x509 or 'certificate' not in x509: + raise ConfigError(f'Missing x509 certificates on L2TP remote-access config') + + verify_pki_x509(ipsec['pki'], x509) + if 'profile' in ipsec: for profile, profile_conf in ipsec['profile'].items(): if 'esp_group' in profile_conf: @@ -191,35 +250,86 @@ def verify(ipsec): raise ConfigError(f"Missing authentication on {profile} profile") if 'remote_access' in ipsec: - for name, ra_conf in ipsec['remote_access'].items(): - if 'esp_group' in ra_conf: - if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']: - raise ConfigError(f"Invalid esp-group on {name} remote-access config") - else: - raise ConfigError(f"Missing esp-group on {name} remote-access config") - - if 'ike_group' in ra_conf: - if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']: - raise ConfigError(f"Invalid ike-group on {name} remote-access config") - else: - raise ConfigError(f"Missing ike-group on {name} remote-access config") - - if 'authentication' not in ra_conf: - raise ConfigError(f"Missing authentication on {name} remote-access config") - - if ra_conf['authentication']['server_mode'] == 'x509': - if 'x509' not in ra_conf['authentication']: - raise ConfigError(f"Missing x509 settings on {name} remote-access config") - - x509 = ra_conf['authentication']['x509'] - - if 'ca_certificate' not in x509 or 'certificate' not in x509: - raise ConfigError(f"Missing x509 certificates on {name} remote-access config") - - verify_pki_x509(ipsec['pki'], x509) - elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret': - if 'pre_shared_secret' not in ra_conf['authentication']: - raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") + if 'connection' in ipsec['remote_access']: + for name, ra_conf in ipsec['remote_access']['connection'].items(): + if 'esp_group' in ra_conf: + if 'esp_group' not in ipsec or ra_conf['esp_group'] not in ipsec['esp_group']: + raise ConfigError(f"Invalid esp-group on {name} remote-access config") + else: + raise ConfigError(f"Missing esp-group on {name} remote-access config") + + if 'ike_group' in ra_conf: + if 'ike_group' not in ipsec or ra_conf['ike_group'] not in ipsec['ike_group']: + raise ConfigError(f"Invalid ike-group on {name} remote-access config") + + ike = ra_conf['ike_group'] + if dict_search(f'ike_group.{ike}.key_exchange', ipsec) != 'ikev2': + raise ConfigError('IPSec remote-access connections requires IKEv2!') + + else: + raise ConfigError(f"Missing ike-group on {name} remote-access config") + + if 'authentication' not in ra_conf: + raise ConfigError(f"Missing authentication on {name} remote-access config") + + if ra_conf['authentication']['server_mode'] == 'x509': + if 'x509' not in ra_conf['authentication']: + raise ConfigError(f"Missing x509 settings on {name} remote-access config") + + x509 = ra_conf['authentication']['x509'] + + if 'ca_certificate' not in x509 or 'certificate' not in x509: + raise ConfigError(f"Missing x509 certificates on {name} remote-access config") + + verify_pki_x509(ipsec['pki'], x509) + elif ra_conf['authentication']['server_mode'] == 'pre-shared-secret': + if 'pre_shared_secret' not in ra_conf['authentication']: + raise ConfigError(f"Missing pre-shared-key on {name} remote-access config") + + + if 'client_mode' in ra_conf['authentication']: + if ra_conf['authentication']['client_mode'] == 'eap-radius': + if 'radius' not in ipsec['remote_access'] or 'server' not in ipsec['remote_access']['radius'] or len(ipsec['remote_access']['radius']['server']) == 0: + raise ConfigError('RADIUS authentication requires at least one server') + + if 'pool' in ra_conf: + if 'dhcp' in ra_conf['pool'] and len(ra_conf['pool']) > 1: + raise ConfigError(f'Can not use both DHCP and a predefined address pool for "{name}"!') + + for pool in ra_conf['pool']: + if pool == 'dhcp': + if dict_search('remote_access.dhcp.server', ipsec) == None: + raise ConfigError('IPSec DHCP server is not configured!') + + elif 'pool' not in ipsec['remote_access'] or pool not in ipsec['remote_access']['pool']: + raise ConfigError(f'Requested pool "{pool}" does not exist!') + + if 'pool' in ipsec['remote_access']: + for pool, pool_config in ipsec['remote_access']['pool'].items(): + if 'prefix' not in pool_config: + raise ConfigError(f'Missing madatory prefix option for pool "{pool}"!') + + if 'name_server' in pool_config: + if len(pool_config['name_server']) > 2: + raise ConfigError(f'Only two name-servers are supported for remote-access pool "{pool}"!') + + for ns in pool_config['name_server']: + v4_addr_and_ns = is_ipv4(ns) and not is_ipv4(pool_config['prefix']) + v6_addr_and_ns = is_ipv6(ns) and not is_ipv6(pool_config['prefix']) + if v4_addr_and_ns or v6_addr_and_ns: + raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and name-server adresses!') + + if 'exclude' in pool_config: + for exclude in pool_config['exclude']: + v4_addr_and_exclude = is_ipv4(exclude) and not is_ipv4(pool_config['prefix']) + v6_addr_and_exclude = is_ipv6(exclude) and not is_ipv6(pool_config['prefix']) + if v4_addr_and_exclude or v6_addr_and_exclude: + raise ConfigError('Must use both IPv4 or IPv6 addresses for pool prefix and exclude prefixes!') + + if 'radius' in ipsec['remote_access'] and 'server' in ipsec['remote_access']['radius']: + for server, server_config in ipsec['remote_access']['radius']['server'].items(): + if 'key' not in server_config: + raise ConfigError(f'Missing RADIUS secret key for server "{server}"') if 'site_to_site' in ipsec and 'peer' in ipsec['site_to_site']: for peer, peer_conf in ipsec['site_to_site']['peer'].items(): @@ -372,7 +482,8 @@ def generate(ipsec): cleanup_pki_files() if not ipsec: - for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, interface_conf, swanctl_conf]: + for config_file in [ipsec_conf, ipsec_secrets, charon_dhcp_conf, + charon_radius_conf, interface_conf, swanctl_conf]: if os.path.isfile(config_file): os.unlink(config_file) render(charon_conf, 'ipsec/charon.tmpl', {'install_routes': default_install_routes}) @@ -389,8 +500,13 @@ def generate(ipsec): if not os.path.exists(KEY_PATH): os.mkdir(KEY_PATH, mode=0o700) - if 'remote_access' in ipsec: - for rw, rw_conf in ipsec['remote_access'].items(): + if 'l2tp' in ipsec: + if 'authentication' in ipsec['l2tp'] and 'x509' in ipsec['l2tp']['authentication']: + generate_pki_files_x509(ipsec['pki'], ipsec['l2tp']['authentication']['x509']) + + if 'remote_access' in ipsec and 'connection' in ipsec['remote_access']: + for rw, rw_conf in ipsec['remote_access']['connection'].items(): + if 'authentication' in rw_conf and 'x509' in rw_conf['authentication']: generate_pki_files_x509(ipsec['pki'], rw_conf['authentication']['x509']) @@ -436,17 +552,10 @@ def generate(ipsec): render(ipsec_secrets, 'ipsec/ipsec.secrets.tmpl', ipsec) render(charon_conf, 'ipsec/charon.tmpl', ipsec) render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.tmpl', ipsec) + render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.tmpl', ipsec) render(interface_conf, 'ipsec/interfaces_use.conf.tmpl', ipsec) render(swanctl_conf, 'ipsec/swanctl.conf.tmpl', ipsec) -def resync_l2tp(ipsec): - if ipsec and not ipsec['l2tp_exists']: - return - - tmp = run('/usr/libexec/vyos/conf_mode/ipsec-settings.py') - if tmp > 0: - print('ERROR: failed to reapply L2TP IPSec settings!') - def resync_nhrp(ipsec): if ipsec and not ipsec['nhrp_exists']: return @@ -470,17 +579,13 @@ def apply(ipsec): if not ipsec: call('sudo ipsec stop') else: - args = '' - if 'auto_update' in ipsec: - args = '--auto-update ' + ipsec['auto_update'] - call(f'sudo ipsec restart {args}') + call('sudo ipsec restart') call('sudo ipsec rereadall') call('sudo ipsec reload') if wait_for_vici_socket(): call('sudo swanctl -q') - resync_l2tp(ipsec) resync_nhrp(ipsec) if __name__ == '__main__': diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index e970d2ef5..9c52f77ca 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -20,7 +20,6 @@ import re from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit -from time import sleep from ipaddress import ip_network diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 2986c3458..f6db196dc 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -19,9 +19,11 @@ from sys import exit from vyos.config import Config from vyos.configdict import dict_merge -from vyos.xml import defaults +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call +from vyos.xml import defaults from vyos import ConfigError from crypt import crypt, mksalt, METHOD_SHA512 @@ -50,6 +52,10 @@ def get_config(): default_values = defaults(base) ocserv = dict_merge(default_values, ocserv) + if ocserv: + ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + return ocserv def verify(ocserv): @@ -72,14 +78,36 @@ def verify(ocserv): raise ConfigError('openconnect authentication credentials required') # Check ssl - if "ssl" in ocserv: - req_cert = ['cert_file', 'key_file'] - for cert in req_cert: - if not cert in ocserv["ssl"]: - raise ConfigError('openconnect ssl {0} required'.format(cert.replace('_', '-'))) - else: + if 'ssl' not in ocserv: raise ConfigError('openconnect ssl required') + if not ocserv['pki'] or 'certificate' not in ocserv['pki']: + raise ConfigError('PKI not configured') + + ssl = ocserv['ssl'] + if 'certificate' not in ssl: + raise ConfigError('openconnect ssl certificate required') + + cert_name = ssl['certificate'] + + if cert_name not in ocserv['pki']['certificate']: + raise ConfigError('Invalid openconnect ssl certificate') + + cert = ocserv['pki']['certificate'][cert_name] + + if 'certificate' not in cert: + raise ConfigError('Missing certificate in PKI') + + if 'private' not in cert or 'key' not in cert['private']: + raise ConfigError('Missing private key in PKI') + + if 'ca_certificate' in ssl: + if 'ca' not in ocserv['pki']: + raise ConfigError('PKI not configured') + + if ssl['ca_certificate'] not in ocserv['pki']['ca']: + raise ConfigError('Invalid openconnect ssl CA certificate') + # Check network settings if "network_settings" in ocserv: if "push_route" in ocserv["network_settings"]: @@ -109,6 +137,29 @@ def generate(ocserv): # Render local users render(ocserv_passwd, 'ocserv/ocserv_passwd.tmpl', ocserv["authentication"]["local_users"]) + if "ssl" in ocserv: + cert_file_path = os.path.join(cfg_dir, 'cert.pem') + cert_key_path = os.path.join(cfg_dir, 'cert.key') + ca_cert_file_path = os.path.join(cfg_dir, 'ca.pem') + + if 'certificate' in ocserv['ssl']: + cert_name = ocserv['ssl']['certificate'] + pki_cert = ocserv['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + if 'private' in pki_cert and 'key' in pki_cert['private']: + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + if 'ca_certificate' in ocserv['ssl']: + ca_name = ocserv['ssl']['ca_certificate'] + pki_ca_cert = ocserv['pki']['ca'][ca_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca_cert['certificate'])) + # Render config render(ocserv_conf, 'ocserv/ocserv_config.tmpl', ocserv) diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 47367f125..d1a71a5ad 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -21,6 +21,8 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configverify import verify_accel_ppp_base_service +from vyos.pki import wrap_certificate +from vyos.pki import wrap_private_key from vyos.template import render from vyos.util import call from vyos.util import dict_search @@ -28,6 +30,7 @@ from vyos import ConfigError from vyos import airbag airbag.enable() +cfg_dir = '/run/accel-pppd' sstp_conf = '/run/accel-pppd/sstp.conf' sstp_chap_secrets = '/run/accel-pppd/sstp.chap-secrets' @@ -42,6 +45,11 @@ def get_config(config=None): # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) + + if sstp: + sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + return sstp def verify(sstp): @@ -56,31 +64,59 @@ def verify(sstp): # # SSL certificate checks # - tmp = dict_search('ssl.ca_cert_file', sstp) - if not tmp: - raise ConfigError(f'SSL CA certificate file required!') - else: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL CA certificate "{tmp}" does not exist!') + if not sstp['pki']: + raise ConfigError('PKI is not configured') - tmp = dict_search('ssl.cert_file', sstp) - if not tmp: - raise ConfigError(f'SSL public key file required!') - else: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL public key "{tmp}" does not exist!') + if 'ssl' not in sstp: + raise ConfigError('SSL missing on SSTP config') - tmp = dict_search('ssl.key_file', sstp) - if not tmp: - raise ConfigError(f'SSL private key file required!') - else: - if not os.path.isfile(tmp): - raise ConfigError(f'SSL private key "{tmp}" does not exist!') + ssl = sstp['ssl'] + + if 'ca_certificate' not in ssl: + raise ConfigError('SSL CA certificate missing on SSTP config') + + if 'certificate' not in ssl: + raise ConfigError('SSL certificate missing on SSTP config') + + cert_name = ssl['certificate'] + + if ssl['ca_certificate'] not in sstp['pki']['ca']: + raise ConfigError('Invalid CA certificate on SSTP config') + + if cert_name not in sstp['pki']['certificate']: + raise ConfigError('Invalid certificate on SSTP config') + + pki_cert = sstp['pki']['certificate'][cert_name] + + if 'private' not in pki_cert or 'key' not in pki_cert['private']: + raise ConfigError('Missing private key for certificate on SSTP config') + + if 'password_protected' in pki_cert['private']: + raise ConfigError('Encrypted private key is not supported on SSTP config') def generate(sstp): if not sstp: return None + cert_file_path = os.path.join(cfg_dir, 'sstp-cert.pem') + cert_key_path = os.path.join(cfg_dir, 'sstp-cert.key') + ca_cert_file_path = os.path.join(cfg_dir, 'sstp-ca.pem') + + cert_name = sstp['ssl']['certificate'] + pki_cert = sstp['pki']['certificate'][cert_name] + + with open(cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_cert['certificate'])) + + with open(cert_key_path, 'w') as f: + f.write(wrap_private_key(pki_cert['private']['key'])) + + ca_cert_name = sstp['ssl']['ca_certificate'] + pki_ca = sstp['pki']['ca'][ca_cert_name] + + with open(ca_cert_file_path, 'w') as f: + f.write(wrap_certificate(pki_ca['certificate'])) + # accel-cmd reload doesn't work so any change results in a restart of the daemon render(sstp_conf, 'accel-ppp/sstp.config.tmpl', sstp) diff --git a/src/conf_mode/vrf.py b/src/conf_mode/vrf.py index 936561edc..c1cfc1dcb 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -18,6 +18,7 @@ import os from sys import exit from json import loads +from tempfile import NamedTemporaryFile from vyos.config import Config from vyos.configdict import node_changed @@ -28,6 +29,8 @@ from vyos.util import call from vyos.util import cmd from vyos.util import dict_search from vyos.util import get_interface_config +from vyos.util import popen +from vyos.util import run from vyos import ConfigError from vyos import frr from vyos import airbag @@ -125,11 +128,17 @@ def verify(vrf): return None + def generate(vrf): render(config_file, 'vrf/vrf.conf.tmpl', vrf) vrf['new_frr_config'] = render_to_string('frr/vrf.frr.tmpl', vrf) + # Render nftables zones config + vrf['nft_vrf_zones'] = NamedTemporaryFile().name + render(vrf['nft_vrf_zones'], 'firewall/nftables-vrf-zones.tmpl', vrf) + return None + def apply(vrf): # Documentation # @@ -151,8 +160,19 @@ def apply(vrf): call(f'ip -4 route del vrf {tmp} unreachable default metric 4278198272') call(f'ip -6 route del vrf {tmp} unreachable default metric 4278198272') call(f'ip link delete dev {tmp}') + # Remove nftables conntrack zone map item + nft_del_element = f'delete element inet vrf_zones ct_iface_map {{ "{tmp}" }}' + cmd(f'nft {nft_del_element}') if 'name' in vrf: + # Separate VRFs in conntrack table + # check if table already exists + _, err = popen('nft list table inet vrf_zones') + # If not, create a table + if err: + cmd(f'nft -f {vrf["nft_vrf_zones"]}') + os.unlink(vrf['nft_vrf_zones']) + for name, config in vrf['name'].items(): table = config['table'] @@ -182,6 +202,9 @@ def apply(vrf): # reconfiguration. state = 'down' if 'disable' in config else 'up' vrf_if.set_admin_state(state) + # Add nftables conntrack zone map item + nft_add_element = f'add element inet vrf_zones ct_iface_map {{ "{name}" : {table} }}' + cmd(f'nft {nft_add_element}') # Linux routing uses rules to find tables - routing targets are then # looked up in those tables. If the lookup got a matching route, the @@ -214,22 +237,25 @@ def apply(vrf): # clean out l3mdev-table rule if present if 1000 in [r.get('priority') for r in list_rules() if r.get('priority') == 1000]: call(f'ip {af} rule del pref 1000') - - # add configuration to FRR - frr_cfg = frr.FRRConfig() - frr_cfg.load_configuration(frr_daemon) - frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') - frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config']) - frr_cfg.commit_configuration(frr_daemon) - - # If FRR config is blank, rerun the blank commit x times due to frr-reload - # behavior/bug not properly clearing out on one commit. - if vrf['new_frr_config'] == '': - for a in range(5): - frr_cfg.commit_configuration(frr_daemon) - - # Save configuration to /run/frr/config/frr.conf - frr.save_configuration() + # Remove VRF zones table from nftables + tmp = run('nft list table inet vrf_zones') + if tmp == 0: + cmd('nft delete table inet vrf_zones') + + # T3694: Somehow we hit a priority inversion here as we need to remove the + # VRF assigned VNI before we can remove a BGP bound VRF instance. Maybe + # move this to an individual helper script that set's up the VNI for the + # given VRF after any routing protocol. + # + # # add configuration to FRR + # frr_cfg = frr.FRRConfig() + # frr_cfg.load_configuration(frr_daemon) + # frr_cfg.modify_section(f'^vrf [a-zA-Z-]*$', '') + # frr_cfg.add_before(r'(interface .*|line vty)', vrf['new_frr_config']) + # frr_cfg.commit_configuration(frr_daemon) + # + # # Save configuration to /run/frr/config/frr.conf + # frr.save_configuration() return None diff --git a/src/conf_mode/vyos_cert.py b/src/conf_mode/vyos_cert.py deleted file mode 100755 index dc7c64684..000000000 --- a/src/conf_mode/vyos_cert.py +++ /dev/null @@ -1,147 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# - -import sys -import os -import tempfile -import pathlib -import ssl - -import vyos.defaults -from vyos.config import Config -from vyos import ConfigError -from vyos.util import cmd - -from vyos import airbag -airbag.enable() - -vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] - -# XXX: this model will need to be extended for tag nodes -dependencies = [ - 'https.py', -] - -def status_self_signed(cert_data): -# check existence and expiration date - path = pathlib.Path(cert_data['conf']) - if not path.is_file(): - return False - path = pathlib.Path(cert_data['crt']) - if not path.is_file(): - return False - path = pathlib.Path(cert_data['key']) - if not path.is_file(): - return False - - # check if certificate is 1/2 past lifetime, with openssl -checkend - end_days = int(cert_data['lifetime']) - end_seconds = int(0.5*60*60*24*end_days) - checkend_cmd = 'openssl x509 -checkend {end} -noout -in {crt}'.format(end=end_seconds, **cert_data) - try: - cmd(checkend_cmd, message='Called process error') - return True - except OSError as err: - if err.errno == 1: - return False - print(err) - # XXX: This seems wrong to continue on failure - # implicitely returning None - -def generate_self_signed(cert_data): - san_config = None - - if ssl.OPENSSL_VERSION_INFO < (1, 1, 1, 0, 0): - san_config = tempfile.NamedTemporaryFile() - with open(san_config.name, 'w') as fd: - fd.write('[req]\n') - fd.write('distinguished_name=req\n') - fd.write('[san]\n') - fd.write('subjectAltName=DNS:vyos\n') - - openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' - '-newkey rsa:4096 -keyout {key} -out {crt} ' - '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' - '-extensions san -config {san_conf}' - ''.format(san_conf=san_config.name, - **cert_data)) - - else: - openssl_req_cmd = ('openssl req -x509 -nodes -days {lifetime} ' - '-newkey rsa:4096 -keyout {key} -out {crt} ' - '-subj "/O=Sentrium/OU=VyOS/CN=vyos" ' - '-addext "subjectAltName=DNS:vyos"' - ''.format(**cert_data)) - - try: - cmd(openssl_req_cmd, message='Called process error') - except OSError as err: - print(err) - # XXX: seems wrong to ignore the failure - - os.chmod('{key}'.format(**cert_data), 0o400) - - with open('{conf}'.format(**cert_data), 'w') as f: - f.write('ssl_certificate {crt};\n'.format(**cert_data)) - f.write('ssl_certificate_key {key};\n'.format(**cert_data)) - - if san_config: - san_config.close() - -def get_config(config=None): - vyos_cert = vyos.defaults.vyos_cert_data - - if config: - conf = config - else: - conf = Config() - if not conf.exists('service https certificates system-generated-certificate'): - return None - else: - conf.set_level('service https certificates system-generated-certificate') - - if conf.exists('lifetime'): - lifetime = conf.return_value('lifetime') - vyos_cert['lifetime'] = lifetime - - return vyos_cert - -def verify(vyos_cert): - return None - -def generate(vyos_cert): - if vyos_cert is None: - return None - - if not status_self_signed(vyos_cert): - generate_self_signed(vyos_cert) - -def apply(vyos_cert): - for dep in dependencies: - command = '{0}/{1}'.format(vyos_conf_scripts_dir, dep) - cmd(command, raising=ConfigError) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 2b66dd9e6..281c9bf2b 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -19,7 +19,15 @@ import os import sys -from vyos.util import call, get_interface_config, get_interface_address +from syslog import syslog +from syslog import openlog +from syslog import LOG_PID +from syslog import LOG_INFO + +from vyos.configquery import ConfigTreeQuery +from vyos.util import call +from vyos.util import get_interface_config +from vyos.util import get_interface_address def get_dhcp_address(interface): addr = get_interface_address(interface) @@ -35,7 +43,8 @@ if __name__ == '__main__': interface = sys.argv[1] dhcp_interface = sys.argv[2] - print(f'vti-up-down: start: {verb} {connection} {interface}') + openlog(ident=f'vti-up-down', logoption=LOG_PID, facility=LOG_INFO) + syslog(f'Interface {interface} {verb} {connection}') if verb in ['up-client', 'up-host']: call('sudo ip route delete default table 220') @@ -43,19 +52,24 @@ if __name__ == '__main__': vti_link = get_interface_config(interface) if not vti_link: - print('vti-up-down: interface not found') + syslog(f'Interface {interface} not found') sys.exit(0) vti_link_up = (vti_link['operstate'] == 'UP' if 'operstate' in vti_link else False) + config = ConfigTreeQuery() + vti_dict = config.get_config_dict(['interfaces', 'vti', interface], + get_first_key=True) + if verb in ['up-client', 'up-host']: if not vti_link_up: if dhcp_interface != 'no': local_ip = get_dhcp_address(dhcp_interface) call(f'sudo ip tunnel change {interface} local {local_ip}') - call(f'sudo ip link set {interface} up') + if 'disable' not in vti_dict: + call(f'sudo ip link set {interface} up') + else: + syslog(f'Interface {interface} is admin down ...') elif verb in ['down-client', 'down-host']: if vti_link_up: call(f'sudo ip link set {interface} down') - - print('vti-up-down: finish')
\ No newline at end of file diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf index 8265e12dc..e03d3a29c 100644 --- a/src/etc/sysctl.d/30-vyos-router.conf +++ b/src/etc/sysctl.d/30-vyos-router.conf @@ -72,6 +72,12 @@ net.ipv4.conf.default.send_redirects=1 # Increase size of buffer for netlink net.core.rmem_max=2097152 +# Remove IPv4 and IPv6 routes from forward information base when link goes down +net.ipv4.conf.all.ignore_routes_with_linkdown=1 +net.ipv4.conf.default.ignore_routes_with_linkdown=1 +net.ipv6.conf.all.ignore_routes_with_linkdown=1 +net.ipv6.conf.default.ignore_routes_with_linkdown=1 + # Enable packet forwarding for IPv6 net.ipv6.conf.all.forwarding=1 @@ -81,6 +87,7 @@ net.ipv6.route.max_size = 262144 # Do not forget IPv6 addresses when a link goes down net.ipv6.conf.default.keep_addr_on_down=1 net.ipv6.conf.all.keep_addr_on_down=1 +net.ipv6.route.skip_notify_on_dev_down=1 # Default value of 20 seems to interfere with larger OSPF and VRRP setups net.ipv4.igmp_max_memberships = 512 diff --git a/src/etc/update-motd.d/99-reboot b/src/etc/update-motd.d/99-reboot new file mode 100755 index 000000000..718be1a7a --- /dev/null +++ b/src/etc/update-motd.d/99-reboot @@ -0,0 +1,7 @@ +#!/bin/vbash +source /opt/vyatta/etc/functions/script-template +if [ -f /run/systemd/shutdown/scheduled ]; then + echo + run show reboot +fi +exit diff --git a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py index ec33906ba..4e7fb117c 100755 --- a/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py +++ b/src/etc/vmware-tools/scripts/resume-vm-default.d/ether-resume.py @@ -25,9 +25,8 @@ def get_config(): c = Config() interfaces = dict() for intf in c.list_effective_nodes('interfaces ethernet'): - # skip interfaces that are disabled or is configured for dhcp + # skip interfaces that are disabled check_disable = f'interfaces ethernet {intf} disable' - check_dhcp = f'interfaces ethernet {intf} address dhcp' if c.exists_effective(check_disable): continue @@ -49,10 +48,10 @@ def apply(config): # add configured addresses to interface for addr in addresses: - if addr == 'dhcp': - cmd = ['dhclient', intf] - else: - cmd = f'ip address add {addr} dev {intf}' + # dhcp is handled by netplug + if addr in ['dhcp', 'dhcpv6']: + continue + cmd = f'ip address add {addr} dev {intf}' syslog.syslog(cmd) run(cmd) diff --git a/src/helpers/strip-private.py b/src/helpers/strip-private.py index 420a039eb..c165d2cba 100755 --- a/src/helpers/strip-private.py +++ b/src/helpers/strip-private.py @@ -116,32 +116,33 @@ if __name__ == "__main__": (True, re.compile(r'pre-shared-secret \S+'), 'pre-shared-secret xxxxxx'), # Strip OSPF md5-key (True, re.compile(r'md5-key \S+'), 'md5-key xxxxxx'), - + # Strip WireGuard private-key + (True, re.compile(r'private-key \S+'), 'private-key xxxxxx'), + # Strip MAC addresses (args.mac, re.compile(r'([0-9a-fA-F]{2}\:){5}([0-9a-fA-F]{2}((\:{0,1})){3})'), r'XX:XX:XX:XX:XX:\2'), # Strip host-name, domain-name, and domain-search (args.hostname, re.compile(r'(host-name|domain-name|domain-search) \S+'), r'\1 xxxxxx'), - + # Strip user-names (args.username, re.compile(r'(user|username|user-id) \S+'), r'\1 xxxxxx'), # Strip full-name (args.username, re.compile(r'(full-name) [ -_A-Z a-z]+'), r'\1 xxxxxx'), - + # Strip DHCP static-mapping and shared network names (args.dhcp, re.compile(r'(shared-network-name|static-mapping) \S+'), r'\1 xxxxxx'), - + # Strip host/domain names (args.domain, re.compile(r' (peer|remote-host|local-host|server) ([\w-]+\.)+[\w-]+'), r' \1 xxxxx.tld'), - + # Strip BGP ASNs (args.asn, re.compile(r'(bgp|remote-as) (\d+)'), r'\1 XXXXXX'), - + # Strip LLDP location parameters (args.lldp, re.compile(r'(altitude|datum|latitude|longitude|ca-value|country-code) (\S+)'), r'\1 xxxxxx'), - + # Strip SNMP location (args.snmp, re.compile(r'(location) \S+'), r'\1 xxxxxx'), ] strip_lines(stripping_rules) - diff --git a/src/helpers/vyos-bridge-sync.py b/src/helpers/vyos-bridge-sync.py deleted file mode 100755 index 097d28d85..000000000 --- a/src/helpers/vyos-bridge-sync.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019 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/>. -# - -# Script is used to synchronize configured bridge interfaces. -# one can add a non existing interface to a bridge group (e.g. VLAN) -# but the vlan interface itself does yet not exist. It should be added -# to the bridge automatically once it's available - -import argparse -from sys import exit -from time import sleep - -from vyos.config import Config -from vyos.util import cmd, run - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - parser.add_argument('-i', '--interface', action='store', help='Interface name which should be added to bridge it is configured for', required=True) - args, unknownargs = parser.parse_known_args() - - conf = Config() - if not conf.list_nodes('interfaces bridge'): - # no bridge interfaces exist .. bail out early - exit(0) - else: - for bridge in conf.list_nodes('interfaces bridge'): - for member_if in conf.list_nodes('interfaces bridge {} member interface'.format(bridge)): - if args.interface == member_if: - command = 'brctl addif "{}" "{}"'.format(bridge, args.interface) - # let interfaces etc. settle - especially required for OpenVPN bridged interfaces - sleep(4) - # XXX: This is ignoring any issue, should be cmd but kept as it - # XXX: during the migration to not cause any regression - run(command) - - exit(0) diff --git a/src/migration-scripts/https/2-to-3 b/src/migration-scripts/https/2-to-3 new file mode 100755 index 000000000..fa29fdd18 --- /dev/null +++ b/src/migration-scripts/https/2-to-3 @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# * Migrate system signed certificate to use PKI + +import sys + +from vyos.configtree import ConfigTree +from vyos.pki import create_certificate +from vyos.pki import create_certificate_request +from vyos.pki import create_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key + +if (len(sys.argv) < 2): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'https', 'certificates'] +pki_base = ['pki'] + +if not config.exists(base + ['system-generated-certificate']): + sys.exit(0) + +def wrapped_pem_to_config_value(pem): + out = [] + for line in pem.strip().split("\n"): + if not line or line.startswith("-----") or line[0] == '#': + continue + out.append(line) + return "".join(out) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +valid_days = 365 +if config.exists(base + ['system-generated-certificate', 'lifetime']): + valid_days = int(config.return_value(base + ['system-generated-certificate', 'lifetime'])) + +key = create_private_key('rsa', 2048) +subject = {'country': 'GB', 'state': 'N/A', 'locality': 'N/A', 'organization': 'VyOS', 'common_name': 'vyos'} +cert_req = create_certificate_request(subject, key, ['vyos']) +cert = create_certificate(cert_req, cert_req, key, valid_days) + +if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', 'generated_https', 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + +if key: + key_pem = encode_private_key(key) + config.set(pki_base + ['certificate', 'generated_https', 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + +if cert and key: + config.set(base + ['certificate'], value='generated_https') +else: + print('Failed to migrate system-generated-certificate from https service') + +config.delete(base + ['system-generated-certificate']) + +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)) + sys.exit(1) diff --git a/src/migration-scripts/interfaces/19-to-20 b/src/migration-scripts/interfaces/19-to-20 index 06e07572f..e96663e54 100755 --- a/src/migration-scripts/interfaces/19-to-20 +++ b/src/migration-scripts/interfaces/19-to-20 @@ -18,62 +18,6 @@ from sys import argv from sys import exit from vyos.configtree import ConfigTree -def migrate_ospf(config, path, interface): - path = path + ['ospf'] - if config.exists(path): - new_base = ['protocols', 'ospf', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip ospf" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ospfv3(config, path, interface): - path = path + ['ospfv3'] - if config.exists(path): - new_base = ['protocols', 'ospfv3', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ipv6 ospfv3" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_rip(config, path, interface): - path = path + ['rip'] - if config.exists(path): - new_base = ['protocols', 'rip', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ip rip" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - -def migrate_ripng(config, path, interface): - path = path + ['ripng'] - if config.exists(path): - new_base = ['protocols', 'ripng', 'interface'] - config.set(new_base) - config.set_tag(new_base) - config.copy(path, new_base + [interface]) - config.delete(path) - - # if "ipv6 ripng" was the only setting, we can clean out the empty - # ip node afterwards - if len(config.list_nodes(path[:-1])) == 0: - config.delete(path[:-1]) - if __name__ == '__main__': if (len(argv) < 1): print("Must specify file name!") @@ -85,57 +29,29 @@ if __name__ == '__main__': config = ConfigTree(config_file) - # - # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" - # - for type in config.list_nodes(['interfaces']): - for interface in config.list_nodes(['interfaces', type]): - ip_base = ['interfaces', type, interface, 'ip'] - ipv6_base = ['interfaces', type, interface, 'ipv6'] - migrate_rip(config, ip_base, interface) - migrate_ripng(config, ipv6_base, interface) - migrate_ospf(config, ip_base, interface) - migrate_ospfv3(config, ipv6_base, interface) - - vif_path = ['interfaces', type, interface, 'vif'] - if config.exists(vif_path): - for vif in config.list_nodes(vif_path): - vif_ip_base = vif_path + [vif, 'ip'] - vif_ipv6_base = vif_path + [vif, 'ipv6'] - ifname = f'{interface}.{vif}' - - migrate_rip(config, vif_ip_base, ifname) - migrate_ripng(config, vif_ipv6_base, ifname) - migrate_ospf(config, vif_ip_base, ifname) - migrate_ospfv3(config, vif_ipv6_base, ifname) - - - vif_s_path = ['interfaces', type, interface, 'vif-s'] - if config.exists(vif_s_path): - for vif_s in config.list_nodes(vif_s_path): - vif_s_ip_base = vif_s_path + [vif_s, 'ip'] - vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] - - # vif-c interfaces MUST be migrated before their parent vif-s - # interface as the migrate_*() functions delete the path! - vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] - if config.exists(vif_c_path): - for vif_c in config.list_nodes(vif_c_path): - vif_c_ip_base = vif_c_path + [vif_c, 'ip'] - vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] - ifname = f'{interface}.{vif_s}.{vif_c}' - - migrate_rip(config, vif_c_ip_base, ifname) - migrate_ripng(config, vif_c_ipv6_base, ifname) - migrate_ospf(config, vif_c_ip_base, ifname) - migrate_ospfv3(config, vif_c_ipv6_base, ifname) - - - ifname = f'{interface}.{vif_s}' - migrate_rip(config, vif_s_ip_base, ifname) - migrate_ripng(config, vif_s_ipv6_base, ifname) - migrate_ospf(config, vif_s_ip_base, ifname) - migrate_ospfv3(config, vif_s_ipv6_base, ifname) + for type in ['tunnel', 'l2tpv3']: + base = ['interfaces', type] + if not config.exists(base): + # Nothing to do + continue + + for interface in config.list_nodes(base): + # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap + encap_path = base + [interface, 'encapsulation'] + if type == 'tunnel' and config.exists(encap_path): + tmp = config.return_value(encap_path) + if tmp == 'gre-bridge': + config.set(encap_path, value='gretap') + + # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address + # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote + local_ip_path = base + [interface, 'local-ip'] + if config.exists(local_ip_path): + config.rename(local_ip_path, 'source-address') + + remote_ip_path = base + [interface, 'remote-ip'] + if config.exists(remote_ip_path): + config.rename(remote_ip_path, 'remote') try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/20-to-21 b/src/migration-scripts/interfaces/20-to-21 index e96663e54..06e07572f 100755 --- a/src/migration-scripts/interfaces/20-to-21 +++ b/src/migration-scripts/interfaces/20-to-21 @@ -18,6 +18,62 @@ from sys import argv from sys import exit from vyos.configtree import ConfigTree +def migrate_ospf(config, path, interface): + path = path + ['ospf'] + if config.exists(path): + new_base = ['protocols', 'ospf', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip ospf" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ospfv3(config, path, interface): + path = path + ['ospfv3'] + if config.exists(path): + new_base = ['protocols', 'ospfv3', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ospfv3" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_rip(config, path, interface): + path = path + ['rip'] + if config.exists(path): + new_base = ['protocols', 'rip', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ip rip" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + +def migrate_ripng(config, path, interface): + path = path + ['ripng'] + if config.exists(path): + new_base = ['protocols', 'ripng', 'interface'] + config.set(new_base) + config.set_tag(new_base) + config.copy(path, new_base + [interface]) + config.delete(path) + + # if "ipv6 ripng" was the only setting, we can clean out the empty + # ip node afterwards + if len(config.list_nodes(path[:-1])) == 0: + config.delete(path[:-1]) + if __name__ == '__main__': if (len(argv) < 1): print("Must specify file name!") @@ -29,29 +85,57 @@ if __name__ == '__main__': config = ConfigTree(config_file) - for type in ['tunnel', 'l2tpv3']: - base = ['interfaces', type] - if not config.exists(base): - # Nothing to do - continue - - for interface in config.list_nodes(base): - # Migrate "interface tunnel <tunX> encapsulation gre-bridge" to gretap - encap_path = base + [interface, 'encapsulation'] - if type == 'tunnel' and config.exists(encap_path): - tmp = config.return_value(encap_path) - if tmp == 'gre-bridge': - config.set(encap_path, value='gretap') - - # Migrate "interface tunnel|l2tpv3 <interface> local-ip" to source-address - # Migrate "interface tunnel|l2tpv3 <interface> remote-ip" to remote - local_ip_path = base + [interface, 'local-ip'] - if config.exists(local_ip_path): - config.rename(local_ip_path, 'source-address') - - remote_ip_path = base + [interface, 'remote-ip'] - if config.exists(remote_ip_path): - config.rename(remote_ip_path, 'remote') + # + # Migrate "interface ethernet eth0 ip ospf" to "protocols ospf interface eth0" + # + for type in config.list_nodes(['interfaces']): + for interface in config.list_nodes(['interfaces', type]): + ip_base = ['interfaces', type, interface, 'ip'] + ipv6_base = ['interfaces', type, interface, 'ipv6'] + migrate_rip(config, ip_base, interface) + migrate_ripng(config, ipv6_base, interface) + migrate_ospf(config, ip_base, interface) + migrate_ospfv3(config, ipv6_base, interface) + + vif_path = ['interfaces', type, interface, 'vif'] + if config.exists(vif_path): + for vif in config.list_nodes(vif_path): + vif_ip_base = vif_path + [vif, 'ip'] + vif_ipv6_base = vif_path + [vif, 'ipv6'] + ifname = f'{interface}.{vif}' + + migrate_rip(config, vif_ip_base, ifname) + migrate_ripng(config, vif_ipv6_base, ifname) + migrate_ospf(config, vif_ip_base, ifname) + migrate_ospfv3(config, vif_ipv6_base, ifname) + + + vif_s_path = ['interfaces', type, interface, 'vif-s'] + if config.exists(vif_s_path): + for vif_s in config.list_nodes(vif_s_path): + vif_s_ip_base = vif_s_path + [vif_s, 'ip'] + vif_s_ipv6_base = vif_s_path + [vif_s, 'ipv6'] + + # vif-c interfaces MUST be migrated before their parent vif-s + # interface as the migrate_*() functions delete the path! + vif_c_path = ['interfaces', type, interface, 'vif-s', vif_s, 'vif-c'] + if config.exists(vif_c_path): + for vif_c in config.list_nodes(vif_c_path): + vif_c_ip_base = vif_c_path + [vif_c, 'ip'] + vif_c_ipv6_base = vif_c_path + [vif_c, 'ipv6'] + ifname = f'{interface}.{vif_s}.{vif_c}' + + migrate_rip(config, vif_c_ip_base, ifname) + migrate_ripng(config, vif_c_ipv6_base, ifname) + migrate_ospf(config, vif_c_ip_base, ifname) + migrate_ospfv3(config, vif_c_ipv6_base, ifname) + + + ifname = f'{interface}.{vif_s}' + migrate_rip(config, vif_s_ip_base, ifname) + migrate_ripng(config, vif_s_ipv6_base, ifname) + migrate_ospf(config, vif_s_ip_base, ifname) + migrate_ospfv3(config, vif_s_ipv6_base, ifname) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/interfaces/22-to-23 b/src/migration-scripts/interfaces/22-to-23 new file mode 100755 index 000000000..93ce9215f --- /dev/null +++ b/src/migration-scripts/interfaces/22-to-23 @@ -0,0 +1,369 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# Migrate Wireguard to store keys in CLI +# Migrate EAPoL to PKI configuration + +import os +import sys +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_dh_parameters +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_dh_parameters +from vyos.pki import encode_private_key +from vyos.util import run + +def wrapped_pem_to_config_value(pem): + out = [] + for line in pem.strip().split("\n"): + if not line or line.startswith("-----") or line[0] == '#': + continue + out.append(line) + return "".join(out) + +def read_file_for_pki(config_auth_path): + full_path = os.path.join(AUTH_DIR, config_auth_path) + output = None + + if os.path.isfile(full_path): + if not os.access(full_path, os.R_OK): + run(f'sudo chmod 644 {full_path}') + + with open(full_path, 'r') as f: + output = f.read() + + return output + +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() + +config = ConfigTree(config_file) + +AUTH_DIR = '/config/auth' +pki_base = ['pki'] + +# OpenVPN +base = ['interfaces', 'openvpn'] + +if config.exists(base): + for interface in config.list_nodes(base): + x509_base = base + [interface, 'tls'] + pki_name = f'openvpn_{interface}' + + if config.exists(base + [interface, 'shared-secret-key-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'shared-secret-key-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_shared' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'shared-secret-key'], value=key_pki_name) + else: + print(f'Failed to migrate shared-secret-key on openvpn interface {interface}') + + config.delete(base + [interface, 'shared-secret-key-file']) + + if not config.exists(base + [interface, 'tls']): + continue + + if config.exists(base + [interface, 'tls', 'auth-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'auth-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_auth' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'auth-key'], value=key_pki_name) + else: + print(f'Failed to migrate auth-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'auth-file']) + + if config.exists(base + [interface, 'tls', 'crypt-file']): + if not config.exists(pki_base + ['openvpn', 'shared-secret']): + config.set(pki_base + ['openvpn', 'shared-secret']) + config.set_tag(pki_base + ['openvpn', 'shared-secret']) + + key_file = config.return_value(base + [interface, 'tls', 'crypt-file']) + key = read_file_for_pki(key_file) + key_pki_name = f'{pki_name}_crypt' + + if key: + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'key'], value=wrapped_pem_to_config_value(key)) + config.set(pki_base + ['openvpn', 'shared-secret', key_pki_name, 'version'], value='1') + config.set(base + [interface, 'tls', 'crypt-key'], value=key_pki_name) + else: + print(f'Failed to migrate crypt-key on openvpn interface {interface}') + + config.delete(base + [interface, 'tls', 'crypt-file']) + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on openvpn interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['crl-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + crl_file = config.return_value(x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + + if os.path.isfile(crl_path): + if not os.access(crl_path, os.R_OK): + run(f'sudo chmod 644 {crl_path}') + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_crl(crl_data, wrap_tags=False) + + if crl: + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + else: + print(f'Failed to migrate CRL on openvpn interface {interface}') + + config.delete(x509_base + ['crl-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on openvpn interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on openvpn interface {interface}') + + config.delete(x509_base + ['key-file']) + + if config.exists(x509_base + ['dh-file']): + if not config.exists(pki_base + ['dh']): + config.set(pki_base + ['dh']) + config.set_tag(pki_base + ['dh']) + + dh_file = config.return_value(x509_base + ['dh-file']) + dh_path = os.path.join(AUTH_DIR, dh_file) + dh = None + + if os.path.isfile(dh_path): + if not os.access(dh_path, os.R_OK): + run(f'sudo chmod 644 {dh_path}') + + with open(dh_path, 'r') as f: + dh_data = f.read() + dh = load_dh_parameters(dh_data, wrap_tags=False) + + if dh: + dh_pem = encode_dh_parameters(dh) + config.set(pki_base + ['dh', pki_name, 'parameters'], value=wrapped_pem_to_config_value(dh_pem)) + config.set(x509_base + ['dh-params'], value=pki_name) + else: + print(f'Failed to migrate DH parameters on openvpn interface {interface}') + + config.delete(x509_base + ['dh-file']) + +# Wireguard +base = ['interfaces', 'wireguard'] + +if config.exists(base): + for interface in config.list_nodes(base): + private_key_path = base + [interface, 'private-key'] + + key_file = 'default' + if config.exists(private_key_path): + key_file = config.return_value(private_key_path) + + full_key_path = f'/config/auth/wireguard/{key_file}/private.key' + + if not os.path.exists(full_key_path): + print(f'Could not find wireguard private key for migration on interface "{interface}"') + continue + + with open(full_key_path, 'r') as f: + key_data = f.read().strip() + config.set(private_key_path, value=key_data) + + for peer in config.list_nodes(base + [interface, 'peer']): + config.rename(base + [interface, 'peer', peer, 'pubkey'], 'public-key') + +# Ethernet EAPoL +base = ['interfaces', 'ethernet'] + +if config.exists(base): + for interface in config.list_nodes(base): + if not config.exists(base + [interface, 'eapol']): + continue + + x509_base = base + [interface, 'eapol'] + pki_name = f'eapol_{interface}' + + if config.exists(x509_base + ['ca-cert-file']): + if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['ca-cert-file']) + + if config.exists(x509_base + ['cert-file']): + if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on eapol config for interface {interface}') + + config.delete(x509_base + ['cert-file']) + + if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on eapol config for interface {interface}') + + config.delete(x509_base + ['key-file']) + +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)) + sys.exit(1) diff --git a/src/migration-scripts/ipsec/5-to-6 b/src/migration-scripts/ipsec/5-to-6 index ba5ce0fca..e9adee01b 100755 --- a/src/migration-scripts/ipsec/5-to-6 +++ b/src/migration-scripts/ipsec/5-to-6 @@ -74,6 +74,17 @@ log_mode = log + ['log-modes'] if config.exists(log_mode): config.rename(log_mode, 'subsystem') +# Rename "ipsec-interfaces interface" to "interface" +base_interfaces = base + ['ipsec-interfaces', 'interface'] +if config.exists(base_interfaces): + config.copy(base_interfaces, base + ['interface']) + config.delete(base_interfaces) + +# Remove deprecated "auto-update" option +tmp = base + ['auto-update'] +if config.exists(tmp): + config.delete(tmp) + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/migration-scripts/l2tp/3-to-4 b/src/migration-scripts/l2tp/3-to-4 new file mode 100755 index 000000000..18eabadec --- /dev/null +++ b/src/migration-scripts/l2tp/3-to-4 @@ -0,0 +1,169 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# - remove primary/secondary identifier from nameserver +# - TODO: remove radius server req-limit + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.util import run + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access', 'ipsec-settings'] +pki_base = ['pki'] + +if not config.exists(base): + exit(0) + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if not config.exists(base + ['authentication', 'x509']): + exit(0) + +x509_base = base + ['authentication', 'x509'] +pki_name = 'l2tp_remote_access' + +if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +if config.exists(x509_base + ['ca-cert-file']): + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on l2tp remote-access config') + + config.delete(x509_base + ['ca-cert-file']) + +if config.exists(x509_base + ['crl-file']): + crl_file = config.return_value(x509_base + ['crl-file']) + crl_path = os.path.join(AUTH_DIR, crl_file) + crl = None + + if os.path.isfile(crl_path): + if not os.access(crl_path, os.R_OK): + run(f'sudo chmod 644 {crl_path}') + + with open(crl_path, 'r') as f: + crl_data = f.read() + crl = load_certificate(crl_data, wrap_tags=False) + + if crl: + crl_pem = encode_certificate(crl) + config.set(pki_base + ['ca', pki_name, 'crl'], value=wrapped_pem_to_config_value(crl_pem)) + else: + print(f'Failed to migrate CRL on l2tp remote-access config') + + config.delete(x509_base + ['crl-file']) + +if config.exists(x509_base + ['server-cert-file']): + cert_file = config.return_value(x509_base + ['server-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on l2tp remote-access config') + + config.delete(x509_base + ['server-cert-file']) + +if config.exists(x509_base + ['server-key-file']): + key_file = config.return_value(x509_base + ['server-key-file']) + key_passphrase = None + + if config.exists(x509_base + ['server-key-password']): + key_passphrase = config.return_value(x509_base + ['server-key-password']) + + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=key_passphrase, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=key_passphrase) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + + if key_passphrase: + config.set(pki_base + ['certificate', pki_name, 'private', 'password-protected']) + config.set(x509_base + ['private-key-passphrase'], value=key_passphrase) + else: + print(f'Failed to migrate private key on l2tp remote-access config') + + config.delete(x509_base + ['server-key-file']) + if config.exists(x509_base + ['server-key-password']): + config.delete(x509_base + ['server-key-password']) + +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/openconnect/0-to-1 b/src/migration-scripts/openconnect/0-to-1 new file mode 100755 index 000000000..83cd09143 --- /dev/null +++ b/src/migration-scripts/openconnect/0-to-1 @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# - Update SSL to use PKI configuration + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.util import run + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'openconnect'] +pki_base = ['pki'] + +if not config.exists(base): + exit(0) + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if not config.exists(base + ['ssl']): + exit(0) + +x509_base = base + ['ssl'] +pki_name = 'openconnect' + +if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +if config.exists(x509_base + ['ca-cert-file']): + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on openconnect config') + + config.delete(x509_base + ['ca-cert-file']) + +if config.exists(x509_base + ['cert-file']): + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on openconnect config') + + config.delete(x509_base + ['cert-file']) + +if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on openconnect config') + + config.delete(x509_base + ['key-file']) + +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/quagga/7-to-8 b/src/migration-scripts/quagga/7-to-8 index 9c277a6f1..15c44924f 100755 --- a/src/migration-scripts/quagga/7-to-8 +++ b/src/migration-scripts/quagga/7-to-8 @@ -14,61 +14,14 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# - T2450: drop interface-route and interface-route6 from "protocols static" +# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths" +# under the IPv4 address-family tree. Reason is we currently have no way in +# configuring this for IPv6 address-family. This mimics the FRR configuration. from sys import argv from sys import exit - from vyos.configtree import ConfigTree -def migrate_interface_route(config, base, path, route_route6): - """ Generic migration function which can be called on every instance of - interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes. - - What we do? - - Drop 'interface-route' or 'interface-route6' and migrate the route unter the - 'route' or 'route6' tag node. - """ - if config.exists(base + path): - for route in config.list_nodes(base + path): - interface = config.list_nodes(base + path + [route, 'next-hop-interface']) - - tmp = base + path + [route, 'next-hop-interface'] - for interface in config.list_nodes(tmp): - new_base = base + [route_route6, route, 'interface'] - config.set(new_base) - config.set_tag(base + [route_route6]) - config.set_tag(new_base) - config.copy(tmp + [interface], new_base + [interface]) - - config.delete(base + path) - -def migrate_route(config, base, path, route_route6): - """ Generic migration function which can be called on every instance of - route, beeing it ipv4, ipv6 or even nested under the static table nodes. - - What we do? - - for consistency reasons rename next-hop-interface to interface - - for consistency reasons rename next-hop-vrf to vrf - """ - if config.exists(base + path): - for route in config.list_nodes(base + path): - next_hop = base + path + [route, 'next-hop'] - if config.exists(next_hop): - for gateway in config.list_nodes(next_hop): - # IPv4 routes calls it next-hop-interface, rename this to - # interface instead so it's consitent with IPv6 - interface_path = next_hop + [gateway, 'next-hop-interface'] - if config.exists(interface_path): - config.rename(interface_path, 'interface') - - # When VRFs got introduced, I (c-po) named it next-hop-vrf, - # we can also call it vrf which is simply shorter. - vrf_path = next_hop + [gateway, 'next-hop-vrf'] - if config.exists(vrf_path): - config.rename(vrf_path, 'vrf') - - if (len(argv) < 2): print("Must specify file name!") exit(1) @@ -78,41 +31,27 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['protocols', 'static'] - +base = ['protocols', 'bgp'] config = ConfigTree(config_file) + if not config.exists(base): # Nothing to do exit(0) -# Migrate interface-route into route -migrate_interface_route(config, base, ['interface-route'], 'route') - -# Migrate interface-route6 into route6 -migrate_interface_route(config, base, ['interface-route6'], 'route6') - -# Cleanup nodes inside route -migrate_route(config, base, ['route'], 'route') - -# Cleanup nodes inside route6 -migrate_route(config, base, ['route6'], 'route6') - -# -# PBR table cleanup -table_path = base + ['table'] -if config.exists(table_path): - for table in config.list_nodes(table_path): - # Migrate interface-route into route - migrate_interface_route(config, table_path + [table], ['interface-route'], 'route') - - # Migrate interface-route6 into route6 - migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6') - - # Cleanup nodes inside route - migrate_route(config, table_path + [table], ['route'], 'route') - - # Cleanup nodes inside route6 - migrate_route(config, table_path + [table], ['route6'], 'route6') +# Check if BGP is actually configured and obtain the ASN +asn_list = config.list_nodes(base) +if asn_list: + # There's always just one BGP node, if any + bgp_base = base + [asn_list[0]] + + maximum_paths = bgp_base + ['maximum-paths'] + if config.exists(maximum_paths): + for bgp_type in ['ebgp', 'ibgp']: + if config.exists(maximum_paths + [bgp_type]): + new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths'] + config.set(new_base) + config.copy(maximum_paths + [bgp_type], new_base + [bgp_type]) + config.delete(maximum_paths) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/quagga/8-to-9 b/src/migration-scripts/quagga/8-to-9 index 15c44924f..38507bd3d 100755 --- a/src/migration-scripts/quagga/8-to-9 +++ b/src/migration-scripts/quagga/8-to-9 @@ -14,14 +14,76 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# - T3391: Migrate "maximum-paths" setting from "protocols bgp asn maximum-paths" -# under the IPv4 address-family tree. Reason is we currently have no way in -# configuring this for IPv6 address-family. This mimics the FRR configuration. +# - T2450: drop interface-route and interface-route6 from "protocols static" from sys import argv from sys import exit + from vyos.configtree import ConfigTree +def migrate_interface_route(config, base, path, route_route6): + """ Generic migration function which can be called on every instance of + interface-route, beeing it ipv4, ipv6 or nested under the "static table" nodes. + + What we do? + - Drop 'interface-route' or 'interface-route6' and migrate the route unter the + 'route' or 'route6' tag node. + """ + if config.exists(base + path): + for route in config.list_nodes(base + path): + interface = config.list_nodes(base + path + [route, 'next-hop-interface']) + + tmp = base + path + [route, 'next-hop-interface'] + for interface in config.list_nodes(tmp): + new_base = base + [route_route6, route, 'interface'] + config.set(new_base) + config.set_tag(base + [route_route6]) + config.set_tag(new_base) + config.copy(tmp + [interface], new_base + [interface]) + + config.delete(base + path) + +def migrate_route(config, base, path, route_route6): + """ Generic migration function which can be called on every instance of + route, beeing it ipv4, ipv6 or even nested under the static table nodes. + + What we do? + - for consistency reasons rename next-hop-interface to interface + - for consistency reasons rename next-hop-vrf to vrf + """ + if config.exists(base + path): + for route in config.list_nodes(base + path): + next_hop = base + path + [route, 'next-hop'] + if config.exists(next_hop): + for gateway in config.list_nodes(next_hop): + # IPv4 routes calls it next-hop-interface, rename this to + # interface instead so it's consitent with IPv6 + interface_path = next_hop + [gateway, 'next-hop-interface'] + if config.exists(interface_path): + config.rename(interface_path, 'interface') + + # When VRFs got introduced, I (c-po) named it next-hop-vrf, + # we can also call it vrf which is simply shorter. + vrf_path = next_hop + [gateway, 'next-hop-vrf'] + if config.exists(vrf_path): + config.rename(vrf_path, 'vrf') + + next_hop = base + path + [route, 'interface'] + if config.exists(next_hop): + for interface in config.list_nodes(next_hop): + # IPv4 routes calls it next-hop-interface, rename this to + # interface instead so it's consitent with IPv6 + interface_path = next_hop + [interface, 'next-hop-interface'] + if config.exists(interface_path): + config.rename(interface_path, 'interface') + + # When VRFs got introduced, I (c-po) named it next-hop-vrf, + # we can also call it vrf which is simply shorter. + vrf_path = next_hop + [interface, 'next-hop-vrf'] + if config.exists(vrf_path): + config.rename(vrf_path, 'vrf') + + if (len(argv) < 2): print("Must specify file name!") exit(1) @@ -31,27 +93,41 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['protocols', 'bgp'] -config = ConfigTree(config_file) +base = ['protocols', 'static'] +config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) -# Check if BGP is actually configured and obtain the ASN -asn_list = config.list_nodes(base) -if asn_list: - # There's always just one BGP node, if any - bgp_base = base + [asn_list[0]] - - maximum_paths = bgp_base + ['maximum-paths'] - if config.exists(maximum_paths): - for bgp_type in ['ebgp', 'ibgp']: - if config.exists(maximum_paths + [bgp_type]): - new_base = bgp_base + ['address-family', 'ipv4-unicast', 'maximum-paths'] - config.set(new_base) - config.copy(maximum_paths + [bgp_type], new_base + [bgp_type]) - config.delete(maximum_paths) +# Migrate interface-route into route +migrate_interface_route(config, base, ['interface-route'], 'route') + +# Migrate interface-route6 into route6 +migrate_interface_route(config, base, ['interface-route6'], 'route6') + +# Cleanup nodes inside route +migrate_route(config, base, ['route'], 'route') + +# Cleanup nodes inside route6 +migrate_route(config, base, ['route6'], 'route6') + +# +# PBR table cleanup +table_path = base + ['table'] +if config.exists(table_path): + for table in config.list_nodes(table_path): + # Migrate interface-route into route + migrate_interface_route(config, table_path + [table], ['interface-route'], 'route') + + # Migrate interface-route6 into route6 + migrate_interface_route(config, table_path + [table], ['interface-route6'], 'route6') + + # Cleanup nodes inside route + migrate_route(config, table_path + [table], ['route'], 'route') + + # Cleanup nodes inside route6 + migrate_route(config, table_path + [table], ['route6'], 'route6') try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/sstp/3-to-4 b/src/migration-scripts/sstp/3-to-4 new file mode 100755 index 000000000..0568f043f --- /dev/null +++ b/src/migration-scripts/sstp/3-to-4 @@ -0,0 +1,136 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# - Update SSL to use PKI configuration + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree +from vyos.pki import load_certificate +from vyos.pki import load_crl +from vyos.pki import load_private_key +from vyos.pki import encode_certificate +from vyos.pki import encode_private_key +from vyos.util import run + +if (len(argv) < 1): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'sstp'] +pki_base = ['pki'] + +if not config.exists(base): + exit(0) + +AUTH_DIR = '/config/auth' + +def wrapped_pem_to_config_value(pem): + return "".join(pem.strip().split("\n")[1:-1]) + +if not config.exists(base + ['ssl']): + exit(0) + +x509_base = base + ['ssl'] +pki_name = 'sstp' + +if not config.exists(pki_base + ['ca']): + config.set(pki_base + ['ca']) + config.set_tag(pki_base + ['ca']) + +if not config.exists(pki_base + ['certificate']): + config.set(pki_base + ['certificate']) + config.set_tag(pki_base + ['certificate']) + +if config.exists(x509_base + ['ca-cert-file']): + cert_file = config.return_value(x509_base + ['ca-cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['ca', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['ca-certificate'], value=pki_name) + else: + print(f'Failed to migrate CA certificate on sstp config') + + config.delete(x509_base + ['ca-cert-file']) + +if config.exists(x509_base + ['cert-file']): + cert_file = config.return_value(x509_base + ['cert-file']) + cert_path = os.path.join(AUTH_DIR, cert_file) + cert = None + + if os.path.isfile(cert_path): + if not os.access(cert_path, os.R_OK): + run(f'sudo chmod 644 {cert_path}') + + with open(cert_path, 'r') as f: + cert_data = f.read() + cert = load_certificate(cert_data, wrap_tags=False) + + if cert: + cert_pem = encode_certificate(cert) + config.set(pki_base + ['certificate', pki_name, 'certificate'], value=wrapped_pem_to_config_value(cert_pem)) + config.set(x509_base + ['certificate'], value=pki_name) + else: + print(f'Failed to migrate certificate on sstp config') + + config.delete(x509_base + ['cert-file']) + +if config.exists(x509_base + ['key-file']): + key_file = config.return_value(x509_base + ['key-file']) + key_path = os.path.join(AUTH_DIR, key_file) + key = None + + if os.path.isfile(key_path): + if not os.access(key_path, os.R_OK): + run(f'sudo chmod 644 {key_path}') + + with open(key_path, 'r') as f: + key_data = f.read() + key = load_private_key(key_data, passphrase=None, wrap_tags=False) + + if key: + key_pem = encode_private_key(key, passphrase=None) + config.set(pki_base + ['certificate', pki_name, 'private', 'key'], value=wrapped_pem_to_config_value(key_pem)) + else: + print(f'Failed to migrate private key on sstp config') + + config.delete(x509_base + ['key-file']) + +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/vrf/0-to-1 b/src/migration-scripts/vrf/0-to-1 index 29b2fab74..2b41ef3c7 100755 --- a/src/migration-scripts/vrf/0-to-1 +++ b/src/migration-scripts/vrf/0-to-1 @@ -91,6 +91,16 @@ for vrf in config.list_nodes(base): if config.exists(vrf_path): config.rename(vrf_path, 'vrf') + next_hop = route_path + [route, 'interface'] + if config.exists(next_hop): + for interface in config.list_nodes(next_hop): + interface_path = next_hop + [interface, 'next-hop-interface'] + if config.exists(interface_path): + config.rename(interface_path, 'interface') + vrf_path = next_hop + [interface, 'next-hop-vrf'] + if config.exists(vrf_path): + config.rename(vrf_path, 'vrf') + # # Cleanup nodes inside route6 # diff --git a/src/migration-scripts/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2 index 20128e957..9bc704e02 100755 --- a/src/migration-scripts/vrf/1-to-2 +++ b/src/migration-scripts/vrf/1-to-2 @@ -49,6 +49,7 @@ for vrf in config.list_nodes(base): new_static_base = vrf_base + [vrf, 'protocols'] config.set(new_static_base) config.copy(static_base, new_static_base + ['static']) + config.set_tag(new_static_base + ['static', 'route']) # Now delete the old configuration config.delete(base) diff --git a/src/migration-scripts/vrf/2-to-3 b/src/migration-scripts/vrf/2-to-3 new file mode 100755 index 000000000..8e0f97141 --- /dev/null +++ b/src/migration-scripts/vrf/2-to-3 @@ -0,0 +1,144 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 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/>. + +# Since connection tracking zones are int16, VRFs tables maximum value must +# be limited to 65535 +# Also, interface names in nftables cannot start from numbers, +# so VRF name should not start from a number + +from sys import argv +from sys import exit +from random import randrange +from random import choice +from string import ascii_lowercase +from vyos.configtree import ConfigTree +import re + + +# Helper function to find all config items with a VRF name +def _search_vrfs(config_commands, vrf_name): + vrf_values = [] + # Regex to find path of config command with old VRF + regex_filter = re.compile(rf'^set (?P<cmd_path>[^\']+vrf) \'{vrf_name}\'$') + # Check each command for VRF value + for config_command in config_commands: + search_result = regex_filter.search(config_command) + if search_result: + # Append VRF command to a list + vrf_values.append(search_result.group('cmd_path').split()) + if vrf_values: + return vrf_values + else: + return None + + +# Helper function to find all config items with a table number +def _search_tables(config_commands, table_num): + table_items = {'table_tags': [], 'table_values': []} + # Regex to find values and nodes with a table number + regex_tags = re.compile(rf'^set (?P<cmd_path>[^\']+table {table_num}) ?.*$') + regex_values = re.compile( + rf'^set (?P<cmd_path>[^\']+table) \'{table_num}\'$') + for config_command in config_commands: + # Search for tag nodes + search_result = regex_tags.search(config_command) + if search_result: + # Append table node path to a tag nodes list + cmd_path = search_result.group('cmd_path').split() + if cmd_path not in table_items['table_tags']: + table_items['table_tags'].append(cmd_path) + # Search for value nodes + search_result = regex_values.search(config_command) + if search_result: + # Append table node path to a value nodes list + table_items['table_values'].append( + search_result.group('cmd_path').split()) + return table_items + + +if (len(argv) < 2): + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vrf', 'name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +# Get a list of all currently used VRFs and tables +vrfs_current = {} +for vrf in config.list_nodes(base): + vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table'])) + +# Check VRF names and table numbers +name_regex = re.compile(r'^\d.*$') +for vrf_name, vrf_table in vrfs_current.items(): + # Check table number + if vrf_table > 65535: + # Find new unused table number + vrfs_current[vrf_name] = None + while not vrfs_current[vrf_name]: + table_random = randrange(100, 65535) + if table_random not in vrfs_current.values(): + vrfs_current[vrf_name] = table_random + # Update number to a new one + config.set(['vrf', 'name', vrf_name, 'table'], + vrfs_current[vrf_name], + replace=True) + # Check config items with old table number and replace to new one + config_commands = config.to_commands().split('\n') + table_config_lines = _search_tables(config_commands, vrf_table) + # Rename table nodes + if table_config_lines.get('table_tags'): + for table_config_path in table_config_lines.get('table_tags'): + config.rename(table_config_path, f'{vrfs_current[vrf_name]}') + # Replace table values + if table_config_lines.get('table_values'): + for table_config_path in table_config_lines.get('table_values'): + config.set(table_config_path, + f'{vrfs_current[vrf_name]}', + replace=True) + + # Check VRF name + if name_regex.match(vrf_name): + vrf_name_new = None + while not vrf_name_new: + vrf_name_rand = f'{choice(ascii_lowercase)}{vrf_name}'[:15] + if vrf_name_rand not in vrfs_current: + vrf_name_new = vrf_name_rand + # Update VRF name to a new one + config.rename(['vrf', 'name', vrf_name], vrf_name_new) + # Check config items with old VRF name and replace to new one + config_commands = config.to_commands().split('\n') + vrf_config_lines = _search_vrfs(config_commands, vrf_name) + # Rename VRF to a new name + if vrf_config_lines: + for vrf_value_path in vrf_config_lines: + config.set(vrf_value_path, vrf_name_new, replace=True) + +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/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py index 1fb61d263..d79b6c024 100755 --- a/src/op_mode/dns_forwarding_statistics.py +++ b/src/op_mode/dns_forwarding_statistics.py @@ -11,7 +11,7 @@ PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' OUT_TMPL_SRC = """ DNS forwarding statistics: -Cache entries: {{ cache_entries -}} +Cache entries: {{ cache_entries }} Cache size: {{ cache_size }} kbytes """ diff --git a/src/op_mode/ikev2_profile_generator.py b/src/op_mode/ikev2_profile_generator.py new file mode 100755 index 000000000..d45525431 --- /dev/null +++ b/src/op_mode/ikev2_profile_generator.py @@ -0,0 +1,230 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2021 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import argparse + +from jinja2 import Template +from sys import exit +from socket import getfqdn +from cryptography.x509.oid import NameOID + +from vyos.config import Config +from vyos.pki import load_certificate +from vyos.template import render_to_string +from vyos.util import ask_input + +# Apple profiles only support one IKE/ESP encryption cipher and hash, whereas +# VyOS comes with a multitude of different proposals for a connection. +# +# We take all available proposals from the VyOS CLI and ask the user which one +# he would like to get enabled in his profile - thus there is limited possibility +# to select a proposal that is not supported on the connection profile. +# +# IOS supports IKE-SA encryption algorithms: +# - DES +# - 3DES +# - AES-128 +# - AES-256 +# - AES-128-GCM +# - AES-256-GCM +# - ChaCha20Poly1305 +# +vyos2apple_cipher = { + '3des' : '3DES', + 'aes128' : 'AES-128', + 'aes256' : 'AES-256', + 'aes128gcm128' : 'AES-128-GCM', + 'aes256gcm128' : 'AES-256-GCM', + 'chacha20poly1305' : 'ChaCha20Poly1305', +} + +# Windows supports IKE-SA encryption algorithms: +# - DES3 +# - AES128 +# - AES192 +# - AES256 +# - GCMAES128 +# - GCMAES192 +# - GCMAES256 +# +vyos2windows_cipher = { + '3des' : 'DES3', + 'aes128' : 'AES128', + 'aes192' : 'AES192', + 'aes256' : 'AES256', + 'aes128gcm128' : 'GCMAES128', + 'aes192gcm128' : 'GCMAES192', + 'aes256gcm128' : 'GCMAES256', +} + +# IOS supports IKE-SA integrity algorithms: +# - SHA1-96 +# - SHA1-160 +# - SHA2-256 +# - SHA2-384 +# - SHA2-512 +# +vyos2apple_integrity = { + 'sha1' : 'SHA1-96', + 'sha1_160' : 'SHA1-160', + 'sha256' : 'SHA2-256', + 'sha384' : 'SHA2-384', + 'sha512' : 'SHA2-512', +} + +# Windows supports IKE-SA integrity algorithms: +# - SHA1-96 +# - SHA1-160 +# - SHA2-256 +# - SHA2-384 +# - SHA2-512 +# +vyos2windows_integrity = { + 'sha1' : 'SHA196', + 'sha256' : 'SHA256', + 'aes128gmac' : 'GCMAES128', + 'aes192gmac' : 'GCMAES192', + 'aes256gmac' : 'GCMAES256', +} + +# IOS 14.2 and later do no support dh-group 1,2 and 5. Supported DH groups would +# be: 14, 15, 16, 17, 18, 19, 20, 21, 31 +ios_supported_dh_groups = ['14', '15', '16', '17', '18', '19', '20', '21', '31'] +# Windows 10 only allows a limited set of DH groups +windows_supported_dh_groups = ['1', '2', '14', '24'] + +parser = argparse.ArgumentParser() +parser.add_argument('--os', const='all', nargs='?', choices=['ios', 'windows'], help='Operating system used for config generation', required=True) +parser.add_argument("--connection", action="store", help='IPsec IKEv2 remote-access connection name from CLI', required=True) +parser.add_argument("--remote", action="store", help='VPN connection remote-address where the client will connect to', required=True) +parser.add_argument("--profile", action="store", help='IKEv2 profile name used in the profile list on the device') +parser.add_argument("--name", action="store", help='VPN connection name as seen in the VPN application later') +args = parser.parse_args() + +ipsec_base = ['vpn', 'ipsec'] +config_base = ipsec_base + ['remote-access', 'connection'] +pki_base = ['pki'] +conf = Config() +if not conf.exists(config_base): + exit('IPSec remote-access is not configured!') + +profile_name = 'VyOS IKEv2 Profile' +if args.profile: + profile_name = args.profile + +vpn_name = 'VyOS IKEv2 VPN' +if args.name: + vpn_name = args.name + +conn_base = config_base + [args.connection] +if not conf.exists(conn_base): + exit(f'IPSec remote-access connection "{args.connection}" does not exist!') + +data = conf.get_config_dict(conn_base, key_mangling=('-', '_'), + get_first_key=True, no_tag_node_value_mangle=True) + +data['profile_name'] = profile_name +data['vpn_name'] = vpn_name +data['remote'] = args.remote +# This is a reverse-DNS style unique identifier used to detect duplicate profiles +tmp = getfqdn().split('.') +tmp = reversed(tmp) +data['rfqdn'] = '.'.join(tmp) + +pki = conf.get_config_dict(pki_base, get_first_key=True) +ca_name = data['authentication']['x509']['ca_certificate'] +cert_name = data['authentication']['x509']['certificate'] + +ca_cert = load_certificate(pki['ca'][ca_name]['certificate']) +cert = load_certificate(pki['certificate'][cert_name]['certificate']) + +data['ca_cn'] = ca_cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value +data['cert_cn'] = cert.subject.get_attributes_for_oid(NameOID.COMMON_NAME)[0].value +data['ca_cert'] = conf.return_value(pki_base + ['ca', ca_name, 'certificate']) + +esp_proposals = conf.get_config_dict(ipsec_base + ['esp-group', data['esp_group'], 'proposal'], + key_mangling=('-', '_'), get_first_key=True) +ike_proposal = conf.get_config_dict(ipsec_base + ['ike-group', data['ike_group'], 'proposal'], + key_mangling=('-', '_'), get_first_key=True) + + +# This script works only for Apple iOS/iPadOS and Windows. Both operating systems +# have different limitations thus we load the limitations based on the operating +# system used. + +vyos2client_cipher = vyos2apple_cipher if args.os == 'ios' else vyos2windows_cipher; +vyos2client_integrity = vyos2apple_integrity if args.os == 'ios' else vyos2windows_integrity; +supported_dh_groups = ios_supported_dh_groups if args.os == 'ios' else windows_supported_dh_groups; + +# Create a dictionary containing client conform IKE settings +ike = {} +count = 1 +for _, proposal in ike_proposal.items(): + if {'dh_group', 'encryption', 'hash'} <= set(proposal): + if (proposal['encryption'] in set(vyos2client_cipher) and + proposal['hash'] in set(vyos2client_integrity) and + proposal['dh_group'] in set(supported_dh_groups)): + + # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme + proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ] + proposal['hash'] = vyos2client_integrity[ proposal['hash'] ] + + ike.update( { str(count) : proposal } ) + count += 1 + +# Create a dictionary containing Apple conform ESP settings +esp = {} +count = 1 +for _, proposal in esp_proposals.items(): + if {'encryption', 'hash'} <= set(proposal): + if proposal['encryption'] in set(vyos2client_cipher) and proposal['hash'] in set(vyos2client_integrity): + # We 're-code' from the VyOS IPSec proposals to the Apple naming scheme + proposal['encryption'] = vyos2client_cipher[ proposal['encryption'] ] + proposal['hash'] = vyos2client_integrity[ proposal['hash'] ] + + esp.update( { str(count) : proposal } ) + count += 1 +try: + if len(ike) > 1: + # Propare the input questions for the user + tmp = '\n' + for number, options in ike.items(): + tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}, DH group {options["dh_group"]}\n' + tmp += '\nSelect one of the above IKE groups: ' + data['ike_encryption'] = ike[ ask_input(tmp, valid_responses=list(ike)) ] + else: + data['ike_encryption'] = ike['1'] + + if len(esp) > 1: + tmp = '\n' + for number, options in esp.items(): + tmp += f'({number}) Encryption {options["encryption"]}, Integrity {options["hash"]}\n' + tmp += '\nSelect one of the above ESP groups: ' + data['esp_encryption'] = esp[ ask_input(tmp, valid_responses=list(esp)) ] + else: + data['esp_encryption'] = esp['1'] + +except KeyboardInterrupt: + exit("Interrupted") + +print('\n\n==== <snip> ====') +if args.os == 'ios': + print(render_to_string('ipsec/ios_profile.tmpl', data)) + print('==== </snip> ====\n') + print('Save the XML from above to a new file named "vyos.mobileconfig" and E-Mail it to your phone.') +elif args.os == 'windows': + print(render_to_string('ipsec/windows_profile.tmpl', data)) + print('==== </snip> ====\n') diff --git a/src/op_mode/ping.py b/src/op_mode/ping.py index 924a889db..2144ab53c 100755 --- a/src/op_mode/ping.py +++ b/src/op_mode/ping.py @@ -51,7 +51,7 @@ options = { 'help': 'Number of seconds before ping exits' }, 'do-not-fragment': { - 'ping': '{command} -M dont', + 'ping': '{command} -M do', 'type': 'noarg', 'help': 'Set DF-bit flag to 1 for no fragmentation' }, @@ -220,6 +220,8 @@ if __name__ == '__main__': try: ip = socket.gethostbyname(host) + except UnicodeError: + sys.exit(f'ping: Unknown host: {host}') except socket.gaierror: ip = host diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 7dbeb4097..297270cf1 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -38,6 +38,8 @@ from vyos.util import cmd CERT_REQ_END = '-----END CERTIFICATE REQUEST-----' +auth_dir = '/config/auth' + # Helper Functions def get_default_values(): @@ -215,7 +217,7 @@ def install_wireguard_key(name, private_key, public_key): print("") print("Public key for use on peer configuration: " + public_key) else: - print("set interfaces wireguard [INTERFACE] peer %s pubkey '%s'" % (name, public_key)) + print("set interfaces wireguard [INTERFACE] peer %s public-key '%s'" % (name, public_key)) print("") print("Private key for use on peer configuration: " + private_key) @@ -230,6 +232,22 @@ def ask_passphrase(): passphrase = ask_input('Enter passphrase:') return passphrase +def write_file(filename, contents): + full_path = os.path.join(auth_dir, filename) + directory = os.path.dirname(full_path) + + if not os.path.exists(directory): + print('Failed to write file: directory does not exist') + return False + + if os.path.exists(full_path) and not ask_yes_no('Do you want to overwrite the existing file?'): + return False + + with open(full_path, 'w') as f: + f.write(contents) + + print(f'File written to {full_path}') + # Generation functions def generate_private_key(): @@ -266,7 +284,7 @@ def parse_san_string(san_string): output.append(value) return output -def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, ask_san=True): +def generate_certificate_request(private_key=None, key_type=None, return_request=False, name=None, install=False, file=False, ask_san=True): if not private_key: private_key, key_type = generate_private_key() @@ -291,14 +309,19 @@ def generate_certificate_request(private_key=None, key_type=None, return_request passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_certificate(cert_req)) print(encode_private_key(private_key, passphrase=passphrase)) return None - print("Certificate request:") - print(encode_certificate(cert_req) + "\n") - install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + if install: + print("Certificate request:") + print(encode_certificate(cert_req) + "\n") + install_certificate(name, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + + if file: + write_file(f'{name}.csr', encode_certificate(cert_req)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ca=False): valid_days = ask_input('Enter how many days certificate will be valid:', default='365' if not is_ca else '1825', numeric_only=True) @@ -307,20 +330,25 @@ def generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False, is_sub_ cert_type = ask_input('Enter certificate type: (client, server)', default='server', valid_responses=['client', 'server']) return create_certificate(cert_req, ca_cert, ca_private_key, valid_days, cert_type, is_ca, is_sub_ca) -def generate_ca_certificate(name, install=False): +def generate_ca_certificate(name, install=False, file=False): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True, ask_san=False) cert = generate_certificate(cert_req, cert_req, private_key, is_ca=True) passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + if install: + install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + + if file: + write_file(f'{name}.pem', encode_certificate(cert)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) -def generate_ca_certificate_sign(name, ca_name, install=False): +def generate_ca_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: @@ -374,14 +402,19 @@ def generate_ca_certificate_sign(name, ca_name, install=False): cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=True, is_sub_ca=True) passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + if install: + install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=True) + + if file: + write_file(f'{name}.pem', encode_certificate(cert)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) -def generate_certificate_sign(name, ca_name, install=False): +def generate_certificate_sign(name, ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: @@ -435,27 +468,37 @@ def generate_certificate_sign(name, ca_name, install=False): cert = generate_certificate(cert_req, ca_cert, ca_private_key, is_ca=False) passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None - install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False) + if install: + install_certificate(name, cert, private_key, key_type, key_passphrase=passphrase, is_ca=False) + + if file: + write_file(f'{name}.pem', encode_certificate(cert)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) -def generate_certificate_selfsign(name, install=False): +def generate_certificate_selfsign(name, install=False, file=False): private_key, key_type = generate_private_key() cert_req = generate_certificate_request(private_key, key_type, return_request=True) cert = generate_certificate(cert_req, cert_req, private_key, is_ca=False) passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_certificate(cert)) print(encode_private_key(private_key, passphrase=passphrase)) return None - install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + if install: + install_certificate(name, cert, private_key=private_key, key_type=key_type, key_passphrase=passphrase, is_ca=False) + + if file: + write_file(f'{name}.pem', encode_certificate(cert)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) -def generate_certificate_revocation_list(ca_name, install=False): +def generate_certificate_revocation_list(ca_name, install=False, file=False): ca_dict = get_config_ca_certificate(ca_name) if not ca_dict: @@ -505,26 +548,35 @@ def generate_certificate_revocation_list(ca_name, install=False): print("Failed to create CRL") return None - if not install: + if not install and not file: print(encode_certificate(crl)) return None - install_crl(ca_name, crl) + if install: + install_crl(ca_name, crl) + + if file: + write_file(f'{name}.crl', encode_certificate(crl)) -def generate_ssh_keypair(name, install=False): +def generate_ssh_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() public_key = private_key.public_key() passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) print("") print(encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) return None - install_ssh_key(name, public_key, private_key, passphrase) + if install: + install_ssh_key(name, public_key, private_key, passphrase) + + if file: + write_file(f'{name}.pem', encode_public_key(public_key, encoding='OpenSSH', key_format='OpenSSH')) + write_file(f'{name}.key', encode_private_key(private_key, encoding='PEM', key_format='OpenSSH', passphrase=passphrase)) -def generate_dh_parameters(name, install=False): +def generate_dh_parameters(name, install=False, file=False): bits = ask_input('Enter DH parameters key size:', default=2048, numeric_only=True) print("Generating parameters...") @@ -534,49 +586,62 @@ def generate_dh_parameters(name, install=False): print("Failed to create DH parameters") return None - if not install: + if not install and not file: print("DH Parameters:") print(encode_dh_parameters(dh_params)) - install_dh_parameters(name, dh_params) + if install: + install_dh_parameters(name, dh_params) + + if file: + write_file(f'{name}.pem', encode_dh_parameters(dh_params)) -def generate_keypair(name, install=False): +def generate_keypair(name, install=False, file=False): private_key, key_type = generate_private_key() public_key = private_key.public_key() passphrase = ask_passphrase() - if not install: + if not install and not file: print(encode_public_key(public_key)) print("") print(encode_private_key(private_key, passphrase=passphrase)) return None - install_keypair(name, key_type, private_key, public_key, passphrase) + if install: + install_keypair(name, key_type, private_key, public_key, passphrase) + + if file: + write_file(f'{name}.pem', encode_public_key(public_key)) + write_file(f'{name}.key', encode_private_key(private_key, passphrase=passphrase)) -def generate_openvpn_key(name, install=False): +def generate_openvpn_key(name, install=False, file=False): result = cmd('openvpn --genkey secret /dev/stdout | grep -o "^[^#]*"') if not result: print("Failed to generate OpenVPN key") return None - if not install: + if not install and not file: print(result) return None - key_lines = result.split("\n") - key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings - key_version = '1' + if install: + key_lines = result.split("\n") + key_data = "".join(key_lines[1:-1]) # Remove wrapper tags and line endings + key_version = '1' + + version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully) + if version_search: + key_version = version_search[1] - version_search = re.search(r'BEGIN OpenVPN Static key V(\d+)', result) # Future-proofing (hopefully) - if version_search: - key_version = version_search[1] + print("Configure mode commands to install OpenVPN key:") + print("set pki openvpn shared-secret %s key '%s'" % (name, key_data)) + print("set pki openvpn shared-secret %s version '%s'" % (name, key_version)) - print("Configure mode commands to install OpenVPN key:") - print("set pki openvpn shared-secret %s key '%s'" % (name, key_data)) - print("set pki openvpn shared-secret %s version '%s'" % (name, key_version)) + if file: + write_file(f'{name}.key', result) -def generate_wireguard_key(name, install=False): +def generate_wireguard_key(name, install=False, file=False): private_key = cmd('wg genkey') public_key = cmd('wg pubkey', input=private_key) @@ -585,17 +650,26 @@ def generate_wireguard_key(name, install=False): print("Public key: " + public_key) return None - install_wireguard_key(name, private_key, public_key) + if install: + install_wireguard_key(name, private_key, public_key) -def generate_wireguard_psk(name, install=False): + if file: + write_file(f'{name}_public.key', public_key) + write_file(f'{name}_private.key', private_key) + +def generate_wireguard_psk(name, install=False, file=False): psk = cmd('wg genpsk') - if not install: + if not install and not file: print("Pre-shared key:") print(psk) return None - install_wireguard_psk(name, psk) + if install: + install_wireguard_psk(name, psk) + + if file: + write_file(f'{name}.key', psk) # Show functions @@ -721,6 +795,7 @@ if __name__ == '__main__': parser.add_argument('--psk', help='Wireguard pre shared key', required=False) # Global + parser.add_argument('--file', help='Write generated keys into specified filename', action='store_true') parser.add_argument('--install', help='Install generated keys into running-config', action='store_true') args = parser.parse_args() @@ -729,31 +804,31 @@ if __name__ == '__main__': if args.action == 'generate': if args.ca: if args.sign: - generate_ca_certificate_sign(args.ca, args.sign, args.install) + generate_ca_certificate_sign(args.ca, args.sign, install=args.install, file=args.file) else: - generate_ca_certificate(args.ca, args.install) + generate_ca_certificate(args.ca, install=args.install, file=args.file) elif args.certificate: if args.sign: - generate_certificate_sign(args.certificate, args.sign, args.install) + generate_certificate_sign(args.certificate, args.sign, install=args.install, file=args.file) elif args.self_sign: - generate_certificate_selfsign(args.certificate, args.install) + generate_certificate_selfsign(args.certificate, install=args.install, file=args.file) else: generate_certificate_request(name=args.certificate, install=args.install) elif args.crl: - generate_certificate_revocation_list(args.crl, args.install) + generate_certificate_revocation_list(args.crl, install=args.install, file=args.file) elif args.ssh: - generate_ssh_keypair(args.ssh, args.install) + generate_ssh_keypair(args.ssh, install=args.install, file=args.file) elif args.dh: - generate_dh_parameters(args.dh, args.install) + generate_dh_parameters(args.dh, install=args.install, file=args.file) elif args.keypair: - generate_keypair(args.keypair, args.install) + generate_keypair(args.keypair, install=args.install, file=args.file) elif args.openvpn: - generate_openvpn_key(args.openvpn, args.install) + generate_openvpn_key(args.openvpn, install=args.install, file=args.file) elif args.wireguard: if args.key: - generate_wireguard_key(args.key, args.install) + generate_wireguard_key(args.key, install=args.install, file=args.file) elif args.psk: - generate_wireguard_psk(args.psk, args.install) + generate_wireguard_psk(args.psk, install=args.install, file=args.file) elif args.action == 'show': if args.ca: show_certificate_authority(None if args.ca == 'all' else args.ca) diff --git a/src/op_mode/show_dhcp.py b/src/op_mode/show_dhcp.py index ff1e3cc56..4df275e04 100755 --- a/src/op_mode/show_dhcp.py +++ b/src/op_mode/show_dhcp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -27,8 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import call - +from vyos.util import is_systemd_service_running lease_file = "/config/dhcpd.leases" pool_key = "shared-networkname" @@ -217,7 +216,7 @@ if __name__ == '__main__': exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcp-server.service') != 0: + if not is_systemd_service_running('isc-dhcp-server.service'): print("WARNING: DHCP server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_dhcpv6.py b/src/op_mode/show_dhcpv6.py index f70f04298..1f987ff7b 100755 --- a/src/op_mode/show_dhcpv6.py +++ b/src/op_mode/show_dhcpv6.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2021 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 @@ -27,7 +27,7 @@ from datetime import datetime from isc_dhcp_leases import Lease, IscDhcpLeases from vyos.config import Config -from vyos.util import call +from vyos.util import is_systemd_service_running lease_file = "/config/dhcpdv6.leases" pool_key = "shared-networkname" @@ -202,7 +202,7 @@ if __name__ == '__main__': exit(0) # if dhcp server is down, inactive leases may still be shown as active, so warn the user. - if call('systemctl -q is-active isc-dhcp-server6.service') != 0: + if not is_systemd_service_running('isc-dhcp-server6.service'): print("WARNING: DHCPv6 server is configured but not started. Data may be stale.") if args.leases: diff --git a/src/op_mode/show_vrf.py b/src/op_mode/show_vrf.py index 94358c6e4..3c7a90205 100755 --- a/src/op_mode/show_vrf.py +++ b/src/op_mode/show_vrf.py @@ -20,12 +20,11 @@ from json import loads from vyos.util import cmd -vrf_out_tmpl = """ -VRF name state mac address flags interfaces +vrf_out_tmpl = """VRF name state mac address flags interfaces -------- ----- ----------- ----- ---------- -{% for v in vrf %} +{%- for v in vrf %} {{"%-16s"|format(v.ifname)}} {{ "%-8s"|format(v.operstate | lower())}} {{"%-17s"|format(v.address | lower())}} {{ v.flags|join(',')|lower()}} {{v.members|join(',')|lower()}} -{% endfor %} +{%- endfor %} """ diff --git a/src/op_mode/wireguard.py b/src/op_mode/wireguard.py deleted file mode 100755 index e08bc983a..000000000 --- a/src/op_mode/wireguard.py +++ /dev/null @@ -1,159 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import argparse -import os -import sys -import shutil -import syslog as sl -import re - -from vyos.config import Config -from vyos.ifconfig import WireGuardIf -from vyos.util import cmd -from vyos.util import run -from vyos.util import check_kmod -from vyos import ConfigError - -dir = r'/config/auth/wireguard' -psk = dir + '/preshared.key' - -k_mod = 'wireguard' - -def generate_keypair(pk, pub): - """ generates a keypair which is stored in /config/auth/wireguard """ - old_umask = os.umask(0o027) - if run(f'wg genkey | tee {pk} | wg pubkey > {pub}') != 0: - raise ConfigError("wireguard key-pair generation failed") - else: - sl.syslog( - sl.LOG_NOTICE, "new keypair wireguard key generated in " + dir) - os.umask(old_umask) - - -def genkey(location): - """ helper function to check, regenerate the keypair """ - pk = "{}/private.key".format(location) - pub = "{}/public.key".format(location) - old_umask = os.umask(0o027) - if os.path.exists(pk) and os.path.exists(pub): - try: - choice = input( - "You already have a wireguard key-pair, do you want to re-generate? [y/n] ") - if choice == 'y' or choice == 'Y': - generate_keypair(pk, pub) - except KeyboardInterrupt: - sys.exit(0) - else: - """ if keypair is bing executed from a running iso """ - if not os.path.exists(location): - run(f'sudo mkdir -p {location}') - run(f'sudo chgrp vyattacfg {location}') - run(f'sudo chmod 750 {location}') - generate_keypair(pk, pub) - os.umask(old_umask) - - -def showkey(key): - """ helper function to show privkey or pubkey """ - if os.path.exists(key): - print (open(key).read().strip()) - else: - print ("{} not found".format(key)) - - -def genpsk(): - """ - generates a preshared key and shows it on stdout, - it's stored only in the cli config - """ - - psk = cmd('wg genpsk') - print(psk) - -def list_key_dirs(): - """ lists all dirs under /config/auth/wireguard """ - if os.path.exists(dir): - nks = next(os.walk(dir))[1] - for nk in nks: - print (nk) - -def del_key_dir(kname): - """ deletes /config/auth/wireguard/<kname> """ - kdir = "{0}/{1}".format(dir,kname) - if not os.path.isdir(kdir): - print ("named keypair {} not found".format(kname)) - return 1 - shutil.rmtree(kdir) - - -if __name__ == '__main__': - check_kmod(k_mod) - parser = argparse.ArgumentParser(description='wireguard key management') - parser.add_argument( - '--genkey', action="store_true", help='generate key-pair') - parser.add_argument( - '--showpub', action="store_true", help='shows public key') - parser.add_argument( - '--showpriv', action="store_true", help='shows private key') - parser.add_argument( - '--genpsk', action="store_true", help='generates preshared-key') - parser.add_argument( - '--location', action="store", help='key location within {}'.format(dir)) - parser.add_argument( - '--listkdir', action="store_true", help='lists named keydirectories') - parser.add_argument( - '--delkdir', action="store_true", help='removes named keydirectories') - parser.add_argument( - '--showinterface', action="store", help='shows interface details') - args = parser.parse_args() - - try: - if args.genkey: - if args.location: - genkey("{0}/{1}".format(dir, args.location)) - else: - genkey("{}/default".format(dir)) - if args.showpub: - if args.location: - showkey("{0}/{1}/public.key".format(dir, args.location)) - else: - showkey("{}/default/public.key".format(dir)) - if args.showpriv: - if args.location: - showkey("{0}/{1}/private.key".format(dir, args.location)) - else: - showkey("{}/default/private.key".format(dir)) - if args.genpsk: - genpsk() - if args.listkdir: - list_key_dirs() - if args.showinterface: - try: - intf = WireGuardIf(args.showinterface, create=False, debug=False) - print(intf.operational.show_interface()) - # the interface does not exists - except Exception: - pass - if args.delkdir: - if args.location: - del_key_dir(args.location) - else: - del_key_dir("default") - - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/op_mode/wireguard_client.py b/src/op_mode/wireguard_client.py index 7a620a01e..7661254da 100755 --- a/src/op_mode/wireguard_client.py +++ b/src/op_mode/wireguard_client.py @@ -38,7 +38,7 @@ To enable this configuration on a VyOS router you can use the following commands {% for addr in address if address is defined %} set interfaces wireguard {{ interface }} peer {{ name }} allowed-ips '{{ addr }}' {% endfor %} -set interfaces wireguard {{ interface }} peer {{ name }} pubkey '{{ pubkey }}' +set interfaces wireguard {{ interface }} peer {{ name }} public-key '{{ pubkey }}' """ client_config = """ diff --git a/src/services/api/graphql/README.graphql b/src/services/api/graphql/README.graphql new file mode 100644 index 000000000..a04138010 --- /dev/null +++ b/src/services/api/graphql/README.graphql @@ -0,0 +1,116 @@ + +Example using GraphQL mutations to configure a DHCP server: + +This assumes that the http-api is running: + +'set service https api' + +One can configure an address on an interface, and configure the DHCP server +to run with that address as default router by requesting these 'mutations' +in the GraphQL playground: + +mutation { + createInterfaceEthernet (data: {interface: "eth1", + address: "192.168.0.1/24", + description: "BOB"}) { + success + errors + data { + address + } + } +} + +mutation { + createDhcpServer(data: {sharedNetworkName: "BOB", + subnet: "192.168.0.0/24", + defaultRouter: "192.168.0.1", + dnsServer: "192.168.0.1", + domainName: "vyos.net", + lease: 86400, + range: 0, + start: "192.168.0.9", + stop: "192.168.0.254", + dnsForwardingAllowFrom: "192.168.0.0/24", + dnsForwardingCacheSize: 0, + dnsForwardingListenAddress: "192.168.0.1"}) { + success + errors + data { + defaultRouter + } + } +} + +The GraphQL playground will be found at: + +https://{{ host_address }}/graphql + +An equivalent curl command to the first example above would be: + +curl -k 'https://192.168.100.168/graphql' -H 'Content-Type: application/json' --data-binary '{"query": "mutation {createInterfaceEthernet (data: {interface: \"eth1\", address: \"192.168.0.1/24\", description: \"BOB\"}) {success errors data {address}}}"}' + +Note that the 'mutation' term is prefaced by 'query' in the curl command. + +What's here: + +services +├── api +│ └── graphql +│ ├── graphql +│ │ ├── directives.py +│ │ ├── __init__.py +│ │ ├── mutations.py +│ │ └── schema +│ │ ├── dhcp_server.graphql +│ │ ├── interface_ethernet.graphql +│ │ └── schema.graphql +│ ├── recipes +│ │ ├── dhcp_server.py +│ │ ├── __init__.py +│ │ ├── interface_ethernet.py +│ │ ├── recipe.py +│ │ └── templates +│ │ ├── dhcp_server.tmpl +│ │ └── interface_ethernet.tmpl +│ └── state.py +├── vyos-configd +├── vyos-hostsd +└── vyos-http-api-server + +The GraphQL library that we are using, Ariadne, advertises itself as a +'schema-first' implementation: define the schema; define resolvers +(handlers) for declared Query and Mutation types (Subscription types are not +currently used). + +In the current approach to a high-level API, we consider the +Jinja2-templated collection of configuration mode 'set'/'delete' commands as +the Ur-data; the GraphQL schema is produced from those files, located in +'api/graphql/recipes/templates'. + +Resolvers for the schema Mutation fields are dynamically generated using a +'directive' added to the respective schema field. The directive, +'@generate', is handled by the class 'DataDirective' in +'api/graphql/graphql/directives.py', which calls the 'make_resolver' function in +'api/graphql/graphql/mutations.py'; the produced resolver calls the appropriate +wrapper in 'api/graphql/recipes', with base class doing the (overridable) +configuration steps of calling all defined 'set'/'delete' commands. + +Integrating the above with vyos-http-api-server is ~10 lines of code. + +What needs to be done: + +• automate generation of schema and wrappers from templated configuration +commands + +• investigate whether the subclassing provided by the named wrappers in +'api/graphql/recipes' is sufficient for use cases which need to modify data + +• encapsulate the manipulation of 'canonical names' which transforms the +prefixed camel-case schema names to various snake-case file/function names + +• consider mechanism for migration of templates: offline vs. on-the-fly + +• define the naming convention for those schema fields that refer to +configuration mode parameters: e.g. how much of the path is needed as prefix +to uniquely define the term diff --git a/src/services/api/graphql/graphql/__init__.py b/src/services/api/graphql/graphql/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/graphql/__init__.py diff --git a/src/services/api/graphql/graphql/directives.py b/src/services/api/graphql/graphql/directives.py new file mode 100644 index 000000000..651421c35 --- /dev/null +++ b/src/services/api/graphql/graphql/directives.py @@ -0,0 +1,17 @@ +from ariadne import SchemaDirectiveVisitor, ObjectType +from . mutations import make_resolver + +class DataDirective(SchemaDirectiveVisitor): + """ + Class providing implementation of 'generate' directive in schema. + + """ + def visit_field_definition(self, field, object_type): + name = f'{field.type}' + # field.type contains the return value of the mutation; trim value + # to produce canonical name + name = name.replace('Result', '', 1) + + func = make_resolver(name) + field.resolve = func + return field diff --git a/src/services/api/graphql/graphql/mutations.py b/src/services/api/graphql/graphql/mutations.py new file mode 100644 index 000000000..98c665c9a --- /dev/null +++ b/src/services/api/graphql/graphql/mutations.py @@ -0,0 +1,60 @@ + +from importlib import import_module +from typing import Any, Dict +from ariadne import ObjectType, convert_kwargs_to_snake_case, convert_camel_case_to_snake +from graphql import GraphQLResolveInfo +from makefun import with_signature + +from .. import state + +mutation = ObjectType("Mutation") + +def make_resolver(mutation_name): + """Dynamically generate a resolver for the mutation named in the + schema by 'mutation_name'. + + Dynamic generation is provided using the package 'makefun' (via the + decorator 'with_signature'), which provides signature-preserving + function wrappers; it provides several improvements over, say, + functools.wraps. + + :raise Exception: + encapsulating ConfigErrors, or internal errors + """ + class_name = mutation_name.replace('create', '', 1).replace('delete', '', 1) + func_base_name = convert_camel_case_to_snake(class_name) + resolver_name = f'resolve_create_{func_base_name}' + func_sig = '(obj: Any, info: GraphQLResolveInfo, data: Dict)' + + @mutation.field(mutation_name) + @convert_kwargs_to_snake_case + @with_signature(func_sig, func_name=resolver_name) + async def func_impl(*args, **kwargs): + try: + if 'data' not in kwargs: + return { + "success": False, + "errors": ['missing data'] + } + + data = kwargs['data'] + session = state.settings['app'].state.vyos_session + + mod = import_module(f'api.graphql.recipes.{func_base_name}') + klass = getattr(mod, class_name) + k = klass(session, data) + k.configure() + + return { + "success": True, + "data": data + } + except Exception as error: + return { + "success": False, + "errors": [str(error)] + } + + return func_impl + + diff --git a/src/services/api/graphql/graphql/schema/dhcp_server.graphql b/src/services/api/graphql/graphql/schema/dhcp_server.graphql new file mode 100644 index 000000000..a7ee75d40 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/dhcp_server.graphql @@ -0,0 +1,35 @@ +input dhcpServerConfigInput { + sharedNetworkName: String + subnet: String + defaultRouter: String + dnsServer: String + domainName: String + lease: Int + range: Int + start: String + stop: String + dnsForwardingAllowFrom: String + dnsForwardingCacheSize: Int + dnsForwardingListenAddress: String +} + +type dhcpServerConfig { + sharedNetworkName: String + subnet: String + defaultRouter: String + dnsServer: String + domainName: String + lease: Int + range: Int + start: String + stop: String + dnsForwardingAllowFrom: String + dnsForwardingCacheSize: Int + dnsForwardingListenAddress: String +} + +type createDhcpServerResult { + data: dhcpServerConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/interface_ethernet.graphql b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql new file mode 100644 index 000000000..fdcf97bad --- /dev/null +++ b/src/services/api/graphql/graphql/schema/interface_ethernet.graphql @@ -0,0 +1,18 @@ +input interfaceEthernetConfigInput { + interface: String + address: String + replace: Boolean = true + description: String +} + +type interfaceEthernetConfig { + interface: String + address: String + description: String +} + +type createInterfaceEthernetResult { + data: interfaceEthernetConfig + success: Boolean! + errors: [String] +} diff --git a/src/services/api/graphql/graphql/schema/schema.graphql b/src/services/api/graphql/graphql/schema/schema.graphql new file mode 100644 index 000000000..8a5e17962 --- /dev/null +++ b/src/services/api/graphql/graphql/schema/schema.graphql @@ -0,0 +1,15 @@ +schema { + query: Query + mutation: Mutation +} + +type Query { + _dummy: String +} + +directive @generate on FIELD_DEFINITION + +type Mutation { + createDhcpServer(data: dhcpServerConfigInput) : createDhcpServerResult @generate + createInterfaceEthernet(data: interfaceEthernetConfigInput) : createInterfaceEthernetResult @generate +} diff --git a/src/services/api/graphql/recipes/__init__.py b/src/services/api/graphql/recipes/__init__.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/src/services/api/graphql/recipes/__init__.py diff --git a/src/services/api/graphql/recipes/dhcp_server.py b/src/services/api/graphql/recipes/dhcp_server.py new file mode 100644 index 000000000..3edb3028e --- /dev/null +++ b/src/services/api/graphql/recipes/dhcp_server.py @@ -0,0 +1,13 @@ + +from . recipe import Recipe + +class DhcpServer(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self.data = transform_data(self.data) + # super().configure() diff --git a/src/services/api/graphql/recipes/interface_ethernet.py b/src/services/api/graphql/recipes/interface_ethernet.py new file mode 100644 index 000000000..f88f5924f --- /dev/null +++ b/src/services/api/graphql/recipes/interface_ethernet.py @@ -0,0 +1,13 @@ + +from . recipe import Recipe + +class InterfaceEthernet(Recipe): + def __init__(self, session, command_file): + super().__init__(session, command_file) + + # Define any custom processing of parameters here by overriding + # configure: + # + # def configure(self): + # self.data = transform_data(self.data) + # super().configure() diff --git a/src/services/api/graphql/recipes/recipe.py b/src/services/api/graphql/recipes/recipe.py new file mode 100644 index 000000000..8fbb9e0bf --- /dev/null +++ b/src/services/api/graphql/recipes/recipe.py @@ -0,0 +1,49 @@ +from ariadne import convert_camel_case_to_snake +import vyos.defaults +from vyos.template import render + +class Recipe(object): + def __init__(self, session, data): + self._session = session + self.data = data + self._name = convert_camel_case_to_snake(type(self).__name__) + + @property + def data(self): + return self.__data + + @data.setter + def data(self, data): + if isinstance(data, dict): + self.__data = data + else: + raise ValueError("data must be of type dict") + + def configure(self): + session = self._session + data = self.data + func_base_name = self._name + + tmpl_file = f'{func_base_name}.tmpl' + cmd_file = f'/tmp/{func_base_name}.cmds' + tmpl_dir = vyos.defaults.directories['api_templates'] + + try: + render(cmd_file, tmpl_file, data, location=tmpl_dir) + commands = [] + with open(cmd_file) as f: + lines = f.readlines() + for line in lines: + commands.append(line.split()) + for cmd in commands: + if cmd[0] == 'set': + session.set(cmd[1:]) + elif cmd[0] == 'delete': + session.delete(cmd[1:]) + else: + raise ValueError('Operation must be "set" or "delete"') + session.commit() + except Exception as error: + raise error + + diff --git a/src/services/api/graphql/recipes/templates/dhcp_server.tmpl b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl new file mode 100644 index 000000000..629ce83c1 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/dhcp_server.tmpl @@ -0,0 +1,9 @@ +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} default-router {{ default_router }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} dns-server {{ dns_server }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} domain-name {{ domain_name }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} lease {{ lease }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} start {{ start }} +set service dhcp-server shared-network-name {{ shared_network_name }} subnet {{ subnet }} range {{ range }} stop {{ stop }} +set service dns forwarding allow-from {{ dns_forwarding_allow_from }} +set service dns forwarding cache-size {{ dns_forwarding_cache_size }} +set service dns forwarding listen-address {{ dns_forwarding_listen_address }} diff --git a/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl new file mode 100644 index 000000000..d9d7ed691 --- /dev/null +++ b/src/services/api/graphql/recipes/templates/interface_ethernet.tmpl @@ -0,0 +1,5 @@ +{% if replace %} +delete interfaces ethernet {{ interface }} address +{% endif %} +set interfaces ethernet {{ interface }} address {{ address }} +set interfaces ethernet {{ interface }} description {{ description }} diff --git a/src/services/api/graphql/state.py b/src/services/api/graphql/state.py new file mode 100644 index 000000000..63db9f4ef --- /dev/null +++ b/src/services/api/graphql/state.py @@ -0,0 +1,4 @@ + +def init(): + global settings + settings = {} diff --git a/src/services/vyos-configd b/src/services/vyos-configd index 6f770b696..670b6e66a 100755 --- a/src/services/vyos-configd +++ b/src/services/vyos-configd @@ -133,8 +133,7 @@ def explicit_print(path, mode, msg): logger.critical("error explicit_print") def run_script(script, config, args) -> int: - if args: - script.argv = args + script.argv = args config.set_level([]) try: c = script.get_config(config) @@ -208,7 +207,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON script_name = None - args = None + args = [] res = re.match(r'^(VYOS_TAGNODE_VALUE=[^/]+)?.*\/([^/]+).py(.*)', data) if res.group(1): @@ -221,7 +220,7 @@ def process_node_data(config, data) -> int: return R_ERROR_DAEMON if res.group(3): args = res.group(3).split() - args.insert(0, f'{script_name}.py') + args.insert(0, f'{script_name}.py') if script_name not in include_set: return R_PASS diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index cbf321dc8..cb4ce4072 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -36,10 +36,16 @@ from starlette.datastructures import FormData, MutableHeaders from starlette.formparsers import FormParser, MultiPartParser from multipart.multipart import parse_options_header +from ariadne import make_executable_schema, load_schema_from_path, snake_case_fallback_resolvers +from ariadne.asgi import GraphQL + import vyos.config +import vyos.defaults from vyos.configsession import ConfigSession, ConfigSessionError +import api.graphql.state + DEFAULT_CONFIG_FILE = '/etc/vyos/http-api.conf' CFG_GROUP = 'vyattacfg' @@ -603,6 +609,25 @@ def show_op(data: ShowModel): return success(res) +### +# GraphQL integration +### + +api.graphql.state.init() + +from api.graphql.graphql.mutations import mutation +from api.graphql.graphql.directives import DataDirective + +api_schema_dir = vyos.defaults.directories['api_schema'] + +type_defs = load_schema_from_path(api_schema_dir) + +schema = make_executable_schema(type_defs, mutation, snake_case_fallback_resolvers, directives={"generate": DataDirective}) + +app.add_route('/graphql', GraphQL(schema, debug=True)) + +### + if __name__ == '__main__': # systemd's user and group options don't work, do it by hand here, # else no one else will be able to commit @@ -626,6 +651,8 @@ if __name__ == '__main__': app.state.vyos_debug = True if server_config['debug'] == 'true' else False app.state.vyos_strict = True if server_config['strict'] == 'true' else False + api.graphql.state.settings['app'] = app + try: uvicorn.run(app, host=server_config["listen_address"], port=int(server_config["port"]), diff --git a/src/systemd/isc-dhcp-server.service b/src/systemd/isc-dhcp-server.service index 9aa70a7cc..a7d86e69c 100644 --- a/src/systemd/isc-dhcp-server.service +++ b/src/systemd/isc-dhcp-server.service @@ -14,10 +14,10 @@ Environment=PID_FILE=/run/dhcp-server/dhcpd.pid CONFIG_FILE=/run/dhcp-server/dhc PIDFile=/run/dhcp-server/dhcpd.pid ExecStartPre=/bin/sh -ec '\ touch ${LEASE_FILE}; \ -chown dhcpd:nogroup ${LEASE_FILE}* ; \ +chown dhcpd:vyattacfg ${LEASE_FILE}* ; \ chmod 664 ${LEASE_FILE}* ; \ -/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' -ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group nogroup -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} +/usr/sbin/dhcpd -4 -t -T -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} ' +ExecStart=/usr/sbin/dhcpd -4 -q -user dhcpd -group vyattacfg -pf ${PID_FILE} -cf ${CONFIG_FILE} -lf ${LEASE_FILE} Restart=always [Install] diff --git a/src/validators/vrf-name b/src/validators/vrf-name index c78a80776..29167c635 100755 --- a/src/validators/vrf-name +++ b/src/validators/vrf-name @@ -33,8 +33,8 @@ if __name__ == '__main__': if vrf == "lo": exit(1) - pattern = "^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|" \ - "vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan)\d+(\.\d+(v.+)?)?$).*$" + pattern = r'^(?!(bond|br|dum|eth|lan|eno|ens|enp|enx|gnv|ipoe|l2tp|l2tpeth|\ + vtun|ppp|pppoe|peth|tun|vti|vxlan|wg|wlan|wwan|\d)\d*(\.\d+)?(v.+)?).*$' if not re.match(pattern, vrf): exit(1) |