diff options
35 files changed, 1279 insertions, 698 deletions
diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2 index 6ab13ab27..629fa952a 100644 --- a/data/templates/dhcp-server/kea-dhcp4.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -1,8 +1,16 @@ { "Dhcp4": { "interfaces-config": { +{% if listen_address is vyos_defined %} + "interfaces": {{ listen_address | kea_address_json }}, + "dhcp-socket-type": "udp", +{% elif listen_interface is vyos_defined %} + "interfaces": {{ listen_interface | tojson }}, + "dhcp-socket-type": "raw", +{% else %} "interfaces": [ "*" ], "dhcp-socket-type": "raw", +{% endif %} "service-sockets-max-retries": 5, "service-sockets-retry-wait-time": 5000 }, diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index a530c14ba..5d17df001 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -1,60 +1,65 @@ ### Autogenerated by service_https.py ### -# Default server configuration -{% for server in server_block_list %} +{% if enable_http_redirect is vyos_defined %} server { - # SSL configuration - # -{% if server.address == '*' %} - listen {{ server.port }} ssl; - listen [::]:{{ server.port }} ssl; -{% else %} - listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl; -{% endif %} + listen 80 default_server; + server_name {{ hostname }}; + return 301 https://$host$request_uri; +} +{% endif %} -{% for name in server.name %} - server_name {{ name }}; +server { +{% if listen_address is vyos_defined %} +{% for address in listen_address %} + listen {{ address | bracketize_ipv6 }}:{{ port }} ssl; {% endfor %} +{% else %} + listen {{ port }} ssl; + listen [::]:{{ port }} ssl; +{% endif %} - root /srv/localui; + server_name {{ hostname }}; + root /srv/localui; -{% if server.vyos_cert %} - ssl_certificate {{ server.vyos_cert.crt }}; - ssl_certificate_key {{ server.vyos_cert.key }}; -{% else %} - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - include snippets/snakeoil.conf; + # SSL configuration +{% if certificates.cert_path is vyos_defined and certificates.key_path is vyos_defined %} + ssl_certificate {{ certificates.cert_path }}; + ssl_certificate_key {{ certificates.key_path }}; +{% if certificates.dh_file is vyos_defined %} + ssl_dhparam {{ certificates.dh_file }}; {% endif %} - ssl_session_cache shared:le_nginx_SSL:10m; - ssl_session_timeout 1440m; - ssl_session_tickets off; +{% else %} + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + include snippets/snakeoil.conf; +{% endif %} - ssl_protocols TLSv1.2 TLSv1.3; - ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; + # Improve HTTPS performance with session resumption + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_protocols {{ 'TLSv' ~ ' TLSv'.join(tls_version) }}; - # proxy settings for HTTP API, if enabled; 503, if not - location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { -{% if server.api %} - proxy_pass http://unix:/run/api.sock; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 600; - proxy_buffering off; -{% else %} - return 503; -{% endif %} -{% if server.allow_client %} -{% for client in server.allow_client %} - allow {{ client }}; -{% endfor %} - deny all; -{% endif %} - } + # From LetsEncrypt + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; - error_page 497 =301 https://$host:{{ server.port }}$request_uri; + # proxy settings for HTTP API, if enabled; 503, if not + location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { +{% if api is vyos_defined %} + proxy_pass http://unix:/run/api.sock; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 600; + proxy_buffering off; +{% else %} + return 503; +{% endif %} +{% if allow_client.address is vyos_defined %} +{% for address in allow_client.address %} + allow {{ address }}; +{% endfor %} + deny all; +{% endif %} + } + error_page 497 =301 https://$host:{{ port }}$request_uri; } - -{% endfor %} diff --git a/data/templates/https/vyos-http-api.service.j2 b/data/templates/https/vyos-http-api.service.j2 index f620b3248..aa4da7666 100644 --- a/data/templates/https/vyos-http-api.service.j2 +++ b/data/templates/https/vyos-http-api.service.j2 @@ -3,6 +3,7 @@ Description=VyOS HTTP API service After=vyos-router.service Requires=vyos-router.service +ConditionPathExists={{ api_config_state }} [Service] ExecStart={{ vrf_command }}/usr/libexec/vyos/services/vyos-http-api-server @@ -20,4 +21,3 @@ Group=vyattacfg [Install] WantedBy=vyos.target - diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index dce61ee77..bb35efe94 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -1698,8 +1698,10 @@ </properties> <children> #include <include/bgp/neighbor-afi-ipv4-unicast.xml.i> + #include <include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i> #include <include/bgp/neighbor-afi-ipv4-vpn.xml.i> #include <include/bgp/neighbor-afi-ipv6-unicast.xml.i> + #include <include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i> #include <include/bgp/neighbor-afi-ipv6-vpn.xml.i> #include <include/bgp/neighbor-afi-l2vpn-evpn.xml.i> </children> diff --git a/interface-definitions/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i new file mode 100644 index 000000000..bd6fc6043 --- /dev/null +++ b/interface-definitions/include/dhcp/option-v4.xml.i @@ -0,0 +1,257 @@ +<!-- include start from dhcp/option-v4.xml.i --> +<node name="option"> + <properties> + <help>DHCP option</help> + </properties> + <children> + #include <include/dhcp/captive-portal.xml.i> + #include <include/dhcp/domain-name.xml.i> + #include <include/dhcp/domain-search.xml.i> + #include <include/dhcp/ntp-server.xml.i> + #include <include/name-server-ipv4.xml.i> + <leafNode name="bootfile-name"> + <properties> + <help>Bootstrap file name</help> + <constraint> + <regex>[[:ascii:]]{1,253}</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="bootfile-server"> + <properties> + <help>Server from which the initial boot file is to be loaded</help> + <valueHelp> + <format>ipv4</format> + <description>Bootfile server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Bootfile server FQDN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + <leafNode name="bootfile-size"> + <properties> + <help>Bootstrap file size</help> + <valueHelp> + <format>u32:1-16</format> + <description>Bootstrap file size in 512 byte blocks</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16"/> + </constraint> + </properties> + </leafNode> + <leafNode name="client-prefix-length"> + <properties> + <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> + <valueHelp> + <format>u32:0-32</format> + <description>DHCP client prefix length must be 0 to 32</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-router"> + <properties> + <help>IP address of default router</help> + <valueHelp> + <format>ipv4</format> + <description>Default router IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ip-forwarding"> + <properties> + <help>Enable IP forwarding on client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ipv6-only-preferred"> + <properties> + <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help> + <valueHelp> + <format>u32</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="pop-server"> + <properties> + <help>IP address of POP3 server</help> + <valueHelp> + <format>ipv4</format> + <description>POP3 server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="server-identifier"> + <properties> + <help>Address for DHCP server identifier</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server identifier IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="smtp-server"> + <properties> + <help>IP address of SMTP server</help> + <valueHelp> + <format>ipv4</format> + <description>SMTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="static-route"> + <properties> + <help>Classless static route destination subnet</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="next-hop"> + <properties> + <help>IP address of router to be used to reach the destination subnet</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of router</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode > + <leafNode name="tftp-server-name"> + <properties> + <help>TFTP server name</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>TFTP server FQDN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time-offset"> + <properties> + <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help> + <valueHelp> + <format>[-]N</format> + <description>Time offset (number, may be negative)</description> + </valueHelp> + <constraint> + <regex>-?[0-9]+</regex> + </constraint> + <constraintErrorMessage>Invalid time offset value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="time-server"> + <properties> + <help>IP address of time server</help> + <valueHelp> + <format>ipv4</format> + <description>Time server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="time-zone"> + <properties> + <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help> + <completionHelp> + <script>timedatectl list-timezones</script> + </completionHelp> + <constraint> + <validator name="timezone" argument="--validate"/> + </constraint> + </properties> + </leafNode> + <node name="vendor-option"> + <properties> + <help>Vendor Specific Options</help> + </properties> + <children> + <node name="ubiquiti"> + <properties> + <help>Ubiquiti specific parameters</help> + </properties> + <children> + <leafNode name="unifi-controller"> + <properties> + <help>Address of UniFi controller</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of UniFi controller</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="wins-server"> + <properties> + <help>IP address for Windows Internet Name Service (WINS) server</help> + <valueHelp> + <format>ipv4</format> + <description>WINS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wpad-url"> + <properties> + <help>Web Proxy Autodiscovery (WPAD) URL</help> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/listen-interface-multi-broadcast.xml.i b/interface-definitions/include/listen-interface-multi-broadcast.xml.i new file mode 100644 index 000000000..b3d5a3ecc --- /dev/null +++ b/interface-definitions/include/listen-interface-multi-broadcast.xml.i @@ -0,0 +1,18 @@ +<!-- include start from listen-interface-multi-broadcast.xml.i --> +<leafNode name="listen-interface"> + <properties> + <help>Interface for DHCP Relay Agent to listen for requests</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pki/dh-params.xml.i b/interface-definitions/include/pki/dh-params.xml.i new file mode 100644 index 000000000..a422df832 --- /dev/null +++ b/interface-definitions/include/pki/dh-params.xml.i @@ -0,0 +1,10 @@ +<!-- include start from pki/certificate-multi.xml.i --> +<leafNode name="dh-params"> + <properties> + <help>Diffie Hellman parameters (server only)</help> + <completionHelp> + <path>pki dh</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index cc84ea8b9..d83172e72 100644 --- a/interface-definitions/include/version/dhcp-server-version.xml.i +++ b/interface-definitions/include/version/dhcp-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcp-server-version.xml.i --> -<syntaxVersion component='dhcp-server' version='8'></syntaxVersion> +<syntaxVersion component='dhcp-server' version='9'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index addf3c1ab..389b5b5c9 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -720,14 +720,7 @@ </leafNode> #include <include/pki/certificate.xml.i> #include <include/pki/ca-certificate-multi.xml.i> - <leafNode name="dh-params"> - <properties> - <help>Diffie Hellman parameters (server only)</help> - <completionHelp> - <path>pki dh</path> - </completionHelp> - </properties> - </leafNode> + #include <include/pki/dh-params.xml.i> <leafNode name="crypt-key"> <properties> <help>Static key to use to authenticate control channel</help> diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index 8e13f9372..27485b6d4 100644 --- a/interface-definitions/service_dhcp-server.xml.in +++ b/interface-definitions/service_dhcp-server.xml.in @@ -74,6 +74,7 @@ </properties> </leafNode> #include <include/listen-address-ipv4.xml.i> + #include <include/listen-interface-multi-broadcast.xml.i> <tagNode name="shared-network-name"> <properties> <help>Name of DHCP shared network</help> @@ -89,12 +90,9 @@ <valueless/> </properties> </leafNode> - #include <include/dhcp/domain-name.xml.i> - #include <include/dhcp/domain-search.xml.i> - #include <include/dhcp/ntp-server.xml.i> + #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> - #include <include/name-server-ipv4.xml.i> <tagNode name="subnet"> <properties> <help>DHCP subnet for shared network</help> @@ -108,73 +106,9 @@ <constraintErrorMessage>Invalid IPv4 subnet definition</constraintErrorMessage> </properties> <children> - <leafNode name="bootfile-name"> - <properties> - <help>Bootstrap file name</help> - <constraint> - <regex>[[:ascii:]]{1,253}</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="bootfile-server"> - <properties> - <help>Server from which the initial boot file is to be loaded</help> - <valueHelp> - <format>ipv4</format> - <description>Bootfile server IPv4 address</description> - </valueHelp> - <valueHelp> - <format>hostname</format> - <description>Bootfile server FQDN</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="fqdn"/> - </constraint> - </properties> - </leafNode> - <leafNode name="bootfile-size"> - <properties> - <help>Bootstrap file size</help> - <valueHelp> - <format>u32:1-16</format> - <description>Bootstrap file size in 512 byte blocks</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-16"/> - </constraint> - </properties> - </leafNode> - #include <include/dhcp/captive-portal.xml.i> - <leafNode name="client-prefix-length"> - <properties> - <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> - <valueHelp> - <format>u32:0-32</format> - <description>DHCP client prefix length must be 0 to 32</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-32"/> - </constraint> - <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="default-router"> - <properties> - <help>IP address of default router</help> - <valueHelp> - <format>ipv4</format> - <description>Default router IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - #include <include/dhcp/domain-name.xml.i> - #include <include/dhcp/domain-search.xml.i> + #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> - #include <include/name-server-ipv4.xml.i> + #include <include/generic-disable-node.xml.i> <leafNode name="exclude"> <properties> <help>IP address to exclude from DHCP lease range</help> @@ -188,12 +122,6 @@ <multi/> </properties> </leafNode> - <leafNode name="ip-forwarding"> - <properties> - <help>Enable IP forwarding on client</help> - <valueless/> - </properties> - </leafNode> <leafNode name="lease"> <properties> <help>Lease timeout in seconds</help> @@ -208,45 +136,6 @@ </properties> <defaultValue>86400</defaultValue> </leafNode> - #include <include/dhcp/ntp-server.xml.i> - <leafNode name="pop-server"> - <properties> - <help>IP address of POP3 server</help> - <valueHelp> - <format>ipv4</format> - <description>POP3 server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="server-identifier"> - <properties> - <help>Address for DHCP server identifier</help> - <valueHelp> - <format>ipv4</format> - <description>DHCP server identifier IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="smtp-server"> - <properties> - <help>IP address of SMTP server</help> - <valueHelp> - <format>ipv4</format> - <description>SMTP server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> <tagNode name="range"> <properties> <help>DHCP lease range</help> @@ -256,6 +145,7 @@ <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v4.xml.i> <leafNode name="start"> <properties> <help>First IP address for DHCP lease range</help> @@ -291,6 +181,8 @@ <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v4.xml.i> + #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> <leafNode name="ip-address"> <properties> @@ -308,143 +200,6 @@ #include <include/interface/duid.xml.i> </children> </tagNode> - <tagNode name="static-route"> - <properties> - <help>Classless static route destination subnet</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - </constraint> - </properties> - <children> - <leafNode name="next-hop"> - <properties> - <help>IP address of router to be used to reach the destination subnet</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of router</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode > - <leafNode name="ipv6-only-preferred"> - <properties> - <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help> - <valueHelp> - <format>u32</format> - <description>Seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="tftp-server-name"> - <properties> - <help>TFTP server name</help> - <valueHelp> - <format>ipv4</format> - <description>TFTP server IPv4 address</description> - </valueHelp> - <valueHelp> - <format>hostname</format> - <description>TFTP server FQDN</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="fqdn"/> - </constraint> - </properties> - </leafNode> - <leafNode name="time-offset"> - <properties> - <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help> - <valueHelp> - <format>[-]N</format> - <description>Time offset (number, may be negative)</description> - </valueHelp> - <constraint> - <regex>-?[0-9]+</regex> - </constraint> - <constraintErrorMessage>Invalid time offset value</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="time-server"> - <properties> - <help>IP address of time server</help> - <valueHelp> - <format>ipv4</format> - <description>Time server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="time-zone"> - <properties> - <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help> - <completionHelp> - <script>timedatectl list-timezones</script> - </completionHelp> - <constraint> - <validator name="timezone" argument="--validate"/> - </constraint> - </properties> - </leafNode> - <node name="vendor-option"> - <properties> - <help>Vendor Specific Options</help> - </properties> - <children> - <node name="ubiquiti"> - <properties> - <help>Ubiquiti specific parameters</help> - </properties> - <children> - <leafNode name="unifi-controller"> - <properties> - <help>Address of UniFi controller</help> - <valueHelp> - <format>ipv4</format> - <description>IP address of UniFi controller</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - </children> - </node> - <leafNode name="wins-server"> - <properties> - <help>IP address for Windows Internet Name Service (WINS) server</help> - <valueHelp> - <format>ipv4</format> - <description>WINS server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="wpad-url"> - <properties> - <help>Web Proxy Autodiscovery (WPAD) URL</help> - </properties> - </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in index 57f36a982..b60c7ff2e 100644 --- a/interface-definitions/service_https.xml.in +++ b/interface-definitions/service_https.xml.in @@ -8,52 +8,6 @@ <priority>1001</priority> </properties> <children> - <tagNode name="virtual-host"> - <properties> - <help>Identifier for virtual host</help> - <constraint> - <regex>[a-zA-Z0-9-_.:]{1,255}</regex> - </constraint> - <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage> - </properties> - <children> - <leafNode name="listen-address"> - <properties> - <help>Address to listen for HTTPS requests</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>HTTPS IPv4 address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>HTTPS IPv6 address</description> - </valueHelp> - <valueHelp> - <format>'*'</format> - <description>any</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - <regex>\*</regex> - </constraint> - </properties> - </leafNode> - #include <include/port-number.xml.i> - <leafNode name='port'> - <defaultValue>443</defaultValue> - </leafNode> - <leafNode name="server-name"> - <properties> - <help>Server names: exact, wildcard, or regex</help> - <multi/> - </properties> - </leafNode> - #include <include/allow-client.xml.i> - </children> - </tagNode> <node name="api"> <properties> <help>VyOS HTTP API configuration</help> @@ -172,19 +126,18 @@ </node> </children> </node> - <node name="api-restrict"> + #include <include/allow-client.xml.i> + <leafNode name="enable-http-redirect"> <properties> - <help>Restrict api proxy to subset of virtual hosts</help> + <help>Enable HTTP to HTTPS redirect</help> + <valueless/> </properties> - <children> - <leafNode name="virtual-host"> - <properties> - <help>Restrict proxy to virtual host(s)</help> - <multi/> - </properties> - </leafNode> - </children> - </node> + </leafNode> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name='port'> + <defaultValue>443</defaultValue> + </leafNode> <node name="certificates"> <properties> <help>TLS certificates</help> @@ -192,8 +145,30 @@ <children> #include <include/pki/ca-certificate.xml.i> #include <include/pki/certificate.xml.i> + #include <include/pki/dh-params.xml.i> </children> </node> + <leafNode name="tls-version"> + <properties> + <help>Specify available TLS version(s)</help> + <completionHelp> + <list>1.2 1.3</list> + </completionHelp> + <valueHelp> + <format>1.2</format> + <description>TLSv1.2</description> + </valueHelp> + <valueHelp> + <format>1.3</format> + <description>TLSv1.3</description> + </valueHelp> + <constraint> + <regex>(1.2|1.3)</regex> + </constraint> + <multi/> + </properties> + <defaultValue>1.2 1.3</defaultValue> + </leafNode> #include <include/interface/vrf.xml.i> </children> </node> diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in index 4a7ffbb66..a4a987aa6 100644 --- a/op-mode-definitions/firewall.xml.in +++ b/op-mode-definitions/firewall.xml.in @@ -396,6 +396,23 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_summary</command> </leafNode> + <node name="zone-policy"> + <properties> + <help>Show zone policy information</help> + </properties> + <children> + <tagNode name="zone"> + <properties> + <help>Show summary of zone policy for a specific zone</help> + <completionHelp> + <path>firewall zone</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/zone.py show --zone $5</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/zone.py show</command> + </node> </children> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_all</command> </node> diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 2f3580571..64145a42e 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -37,6 +37,7 @@ directories = { } config_status = '/tmp/vyos-config-status' +api_config_state = '/run/http-api-state' cfg_group = 'vyattacfg' @@ -45,14 +46,3 @@ cfg_vintage = 'vyos' commit_lock = '/opt/vyatta/config/.lock' component_version_json = os.path.join(directories['data'], 'component-versions.json') - -https_data = { - 'listen_addresses' : { '*': ['_'] } -} - -vyos_cert_data = { - 'conf' : '/etc/nginx/snippets/vyos-cert.conf', - 'crt' : '/etc/ssl/certs/vyos-selfsigned.crt', - 'key' : '/etc/ssl/private/vyos-selfsign', - 'lifetime' : '365', -} diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 819fe16a9..3d8cf3637 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -25,7 +25,7 @@ from vyos.template import netmask_from_cidr from vyos.utils.dict import dict_search_args from vyos.utils.file import file_permissions from vyos.utils.file import read_file -from vyos.utils.process import cmd +from vyos.utils.process import run kea4_options = { 'name_server': 'domain-name-servers', @@ -92,17 +92,28 @@ def kea_parse_options(config): options.append({'name': 'pcode', 'data': tz_string}) options.append({'name': 'tcode', 'data': config['time_zone']}) + unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') + if unifi_controller: + options.append({ + 'name': 'unifi-controller', + 'data': unifi_controller, + 'space': 'ubnt' + }) + return options def kea_parse_subnet(subnet, config): out = {'subnet': subnet} - options = kea_parse_options(config) + options = [] + + if 'option' in config: + out['option-data'] = kea_parse_options(config['option']) - if 'bootfile_name' in config: - out['boot-file-name'] = config['bootfile_name'] + if 'bootfile_name' in config['option']: + out['boot-file-name'] = config['option']['bootfile_name'] - if 'bootfile_server' in config: - out['next-server'] = config['bootfile_server'] + if 'bootfile_server' in config['option']: + out['next-server'] = config['option']['bootfile_server'] if 'lease' in config: out['valid-lifetime'] = int(config['lease']) @@ -112,7 +123,20 @@ def kea_parse_subnet(subnet, config): pools = [] for num, range_config in config['range'].items(): start, stop = range_config['start'], range_config['stop'] - pools.append({'pool': f'{start} - {stop}'}) + pool = { + 'pool': f'{start} - {stop}' + } + + if 'option' in range_config: + pool['option-data'] = kea_parse_options(range_config['option']) + + if 'bootfile_name' in range_config['option']: + pool['boot-file-name'] = range_config['option']['bootfile_name'] + + if 'bootfile_server' in range_config['option']: + pool['next-server'] = range_config['option']['bootfile_server'] + + pools.append(pool) out['pools'] = pools if 'static_mapping' in config: @@ -134,19 +158,17 @@ def kea_parse_subnet(subnet, config): if 'ip_address' in host_config: reservation['ip-address'] = host_config['ip_address'] - reservations.append(reservation) - out['reservations'] = reservations + if 'option' in host_config: + reservation['option-data'] = kea_parse_options(host_config['option']) - unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') - if unifi_controller: - options.append({ - 'name': 'unifi-controller', - 'data': unifi_controller, - 'space': 'ubnt' - }) + if 'bootfile_name' in host_config['option']: + reservation['boot-file-name'] = host_config['option']['bootfile_name'] - if options: - out['option-data'] = options + if 'bootfile_server' in host_config['option']: + reservation['next-server'] = host_config['option']['bootfile_server'] + + reservations.append(reservation) + out['reservations'] = reservations return out @@ -293,7 +315,7 @@ def _ctrl_socket_command(path, command, args=None): return None if file_permissions(path) != '0775': - cmd(f'sudo chmod 775 {path}') + run(f'sudo chmod 775 {path}') with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(path) diff --git a/python/vyos/template.py b/python/vyos/template.py index 29ea0889b..1368f1f61 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -786,6 +786,23 @@ def range_to_regex(num_range): regex = range_to_regex(num_range) return f'({regex})' +@register_filter('kea_address_json') +def kea_address_json(addresses): + from json import dumps + from vyos.utils.network import is_addr_assigned + + out = [] + + for address in addresses: + ifname = is_addr_assigned(address, return_ifname=True) + + if not ifname: + continue + + out.append(f'{ifname}/{address}') + + return dumps(out) + @register_filter('kea_failover_json') def kea_failover_json(config): from json import dumps @@ -842,15 +859,22 @@ def kea_shared_network_json(shared_networks): 'authoritative': ('authoritative' in config), 'subnet4': [] } - options = kea_parse_options(config) + + if 'option' in config: + network['option-data'] = kea_parse_options(config['option']) + + if 'bootfile_name' in config['option']: + network['boot-file-name'] = config['option']['bootfile_name'] + + if 'bootfile_server' in config['option']: + network['next-server'] = config['option']['bootfile_server'] if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): + if 'disable' in subnet_config: + continue network['subnet4'].append(kea_parse_subnet(subnet, subnet_config)) - if options: - network['option-data'] = options - out.append(network) return dumps(out, indent=4) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 997ee6309..b782e0bd8 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -308,7 +308,7 @@ def is_ipv6_link_local(addr): return False -def is_addr_assigned(ip_address, vrf=None) -> bool: +def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str: """ Verify if the given IPv4/IPv6 address is assigned to any interface """ from netifaces import interfaces from vyos.utils.network import get_interface_config @@ -323,7 +323,7 @@ def is_addr_assigned(ip_address, vrf=None) -> bool: continue if is_intf_addr_assigned(interface, ip_address): - return True + return interface if return_ifname else True return False diff --git a/smoketest/config-tests/basic-api-service b/smoketest/config-tests/basic-api-service index 1d2dc3472..dc54929b9 100644 --- a/smoketest/config-tests/basic-api-service +++ b/smoketest/config-tests/basic-api-service @@ -4,15 +4,11 @@ set interfaces loopback lo set service ntp server time1.vyos.net set service ntp server time2.vyos.net set service ntp server time3.vyos.net +set service https allow-client address '172.16.0.0/12' +set service https allow-client address '192.168.0.0/16' +set service https allow-client address '10.0.0.0/8' +set service https allow-client address '2001:db8::/32' set service https api keys id 1 key 'S3cur3' -set service https virtual-host bar allow-client address '172.16.0.0/12' -set service https virtual-host bar port '5555' -set service https virtual-host foo allow-client address '10.0.0.0/8' -set service https virtual-host foo allow-client address '2001:db8::/32' -set service https virtual-host foo port '7777' -set service https virtual-host baz allow-client address '192.168.0.0/16' -set service https virtual-host baz port '6666' -set service https virtual-host baz server-name 'baz' set system config-management commit-revisions '100' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' diff --git a/smoketest/configs/basic-api-service b/smoketest/configs/basic-api-service index f5b56ac98..f997ccd73 100644 --- a/smoketest/configs/basic-api-service +++ b/smoketest/configs/basic-api-service @@ -29,6 +29,7 @@ service { allow-client { address 192.168.0.0/16 } + listen-address "*" listen-port 6666 server-name baz } diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index 2ccc63b2c..02beafb26 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -19,6 +19,8 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.utils.file import read_file + base_path = ['pki'] valid_ca_cert = """ @@ -153,10 +155,10 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestPKI, cls).setUpClass() - # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['service', 'https']) def tearDown(self): self.cli_delete(base_path) @@ -181,68 +183,72 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_invalid_ca_valid_certificate(self): - self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_cert.replace('\n','')]) + self.cli_set(base_path + ['ca', 'invalid-ca', 'certificate', valid_cert.replace('\n','')]) with self.assertRaises(ConfigSessionError): self.cli_commit() def test_certificate_in_use(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'smoketest' + + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name]) self.cli_commit() - self.cli_delete(base_path + ['certificate', 'smoketest']) + self.cli_delete(base_path + ['certificate', cert_name]) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(['service', 'https', 'certificates', 'certificate']) def test_certificate_https_update(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'smoke-test_foo' + cert_path = f'/run/nginx/certs/{cert_name}_cert.pem' + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name]) self.cli_commit() cert_data = None - with open('/etc/ssl/certs/smoketest.pem') as f: - cert_data = f.read() + cert_data = read_file(cert_path) - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')]) self.cli_commit() - with open('/etc/ssl/certs/smoketest.pem') as f: - self.assertNotEqual(cert_data, f.read()) + self.assertNotEqual(cert_data, read_file(cert_path)) self.cli_delete(['service', 'https', 'certificates', 'certificate']) def test_certificate_eapol_update(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'eapol' + interface = 'eth1' + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['interfaces', 'ethernet', 'eth1', 'eapol', 'certificate', 'smoketest']) + self.cli_set(['interfaces', 'ethernet', interface, 'eapol', 'certificate', cert_name]) self.cli_commit() cert_data = None - with open('/run/wpa_supplicant/eth1_cert.pem') as f: + with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f: cert_data = f.read() - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')]) self.cli_commit() - with open('/run/wpa_supplicant/eth1_cert.pem') as f: + with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f: self.assertNotEqual(cert_data, f.read()) - self.cli_delete(['interfaces', 'ethernet', 'eth1', 'eapol']) + self.cli_delete(['interfaces', 'ethernet', interface, 'eapol']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index ebc9eeaaa..d5efae12c 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1151,7 +1151,43 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' locator {locator_name}', frrconfig) self.assertIn(f' sid vpn per-vrf export {sid}', frrconfig) - def test_bgp_25_bmp(self): + def test_bgp_25_ipv4_ipv6_labeled_unicast_peer_group(self): + pg_ipv4 = 'foo4' + pg_ipv6 = 'foo6' + + ipv4_max_prefix = '20' + ipv6_max_prefix = '200' + ipv4_prefix = '192.0.2.0/24' + ipv6_prefix = '2001:db8:1000::/64' + + self.cli_set(base_path + ['listen', 'range', ipv4_prefix, 'peer-group', pg_ipv4]) + self.cli_set(base_path + ['listen', 'range', ipv6_prefix, 'peer-group', pg_ipv6]) + + self.cli_set(base_path + ['peer-group', pg_ipv4, 'address-family', 'ipv4-labeled-unicast', 'maximum-prefix', ipv4_max_prefix]) + self.cli_set(base_path + ['peer-group', pg_ipv4, 'remote-as', 'external']) + self.cli_set(base_path + ['peer-group', pg_ipv6, 'address-family', 'ipv6-labeled-unicast', 'maximum-prefix', ipv6_max_prefix]) + self.cli_set(base_path + ['peer-group', pg_ipv6, 'remote-as', 'external']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig) + self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) + self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig) + self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) + + afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config) + self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config) + + afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast') + self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) + self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) + + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' target_port = '5000' diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index bf0c09965..6f24d40ec 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -32,6 +32,7 @@ CTRL_PROCESS_NAME = 'kea-ctrl-agent' KEA4_CONF = '/run/kea/kea-dhcp4.conf' KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' base_path = ['service', 'dhcp-server'] +interface = 'dum8765' subnet = '192.0.2.0/25' router = inc_ip(subnet, 1) dns_1 = inc_ip(subnet, 2) @@ -46,11 +47,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) cidr_mask = subnet.split('/')[-1] - cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}']) + cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']) @classmethod def tearDownClass(cls): - cls.cli_delete(cls, ['interfaces', 'dummy', 'dum8765']) + cls.cli_delete(cls, ['interfaces', 'dummy', interface]) super(TestServiceDHCPServer, cls).tearDownClass() def tearDown(self): @@ -95,12 +96,14 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_start = inc_ip(subnet, 40) range_1_stop = inc_ip(subnet, 50) + self.cli_set(base_path + ['listen-interface', interface]) + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -116,6 +119,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface]) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) @@ -165,29 +169,26 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) - self.cli_set(pool + ['ip-forwarding']) - self.cli_set(pool + ['smtp-server', smtp_server]) - self.cli_set(pool + ['pop-server', smtp_server]) - self.cli_set(pool + ['time-server', time_server]) - self.cli_set(pool + ['tftp-server-name', tftp_server]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + self.cli_set(pool + ['option', 'ip-forwarding']) + self.cli_set(pool + ['option', 'smtp-server', smtp_server]) + self.cli_set(pool + ['option', 'pop-server', smtp_server]) + self.cli_set(pool + ['option', 'time-server', time_server]) + self.cli_set(pool + ['option', 'tftp-server-name', tftp_server]) for search in search_domains: - self.cli_set(pool + ['domain-search', search]) - self.cli_set(pool + ['bootfile-name', bootfile_name]) - self.cli_set(pool + ['bootfile-server', bootfile_server]) - self.cli_set(pool + ['wpad-url', wpad]) - self.cli_set(pool + ['server-identifier', server_identifier]) + self.cli_set(pool + ['option', 'domain-search', search]) + self.cli_set(pool + ['option', 'bootfile-name', bootfile_name]) + self.cli_set(pool + ['option', 'bootfile-server', bootfile_server]) + self.cli_set(pool + ['option', 'wpad-url', wpad]) + self.cli_set(pool + ['option', 'server-identifier', server_identifier]) - self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) - self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred]) - self.cli_set(pool + ['time-zone', 'Europe/London']) + self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) + self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred]) + self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) - # check validate() - No DHCP address range or active static-mapping set - with self.assertRaises(ConfigSessionError): - self.cli_commit() self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -281,16 +282,81 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dhcp_single_pool_options_scoped(self): + shared_net_name = 'SMOKE-2' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + + range_router = inc_ip(subnet, 5) + range_dns_1 = inc_ip(subnet, 6) + range_dns_2 = inc_ip(subnet, 7) + + shared_network = base_path + ['shared-network-name', shared_net_name] + pool = shared_network + ['subnet', subnet] + # we use the first subnet IP address as default gateway + self.cli_set(shared_network + ['option', 'default-router', router]) + self.cli_set(shared_network + ['option', 'name-server', dns_1]) + self.cli_set(shared_network + ['option', 'name-server', dns_2]) + self.cli_set(shared_network + ['option', 'domain-name', domain_name]) + + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', range_router]) + self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_1]) + self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_2]) + + # commit changes + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + + # Verify shared-network options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + # Verify range options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'routers', 'data': range_router}) + + # Verify pool + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}') + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dhcp_single_pool_static_mapping(self): shared_net_name = 'SMOKE-2' domain_name = 'private' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -309,6 +375,13 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'client1', 'duid']) + # cannot have mappings with duplicate IP addresses + with self.assertRaises(ConfigSessionError): + self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:00:01']) + self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]) + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'dupe1']) + # commit changes self.cli_commit() @@ -365,9 +438,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) self.cli_set(pool + ['lease', lease_time]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) @@ -448,7 +521,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet] - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -490,7 +563,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_start_excl = inc_ip(exclude_addr, 1) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet] - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', exclude_addr]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -535,7 +608,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_stop = '10.0.250.255' pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet] - self.cli_set(pool + ['default-router', relay_router]) + self.cli_set(pool + ['option', 'default-router', relay_router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -545,6 +618,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}']) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY') self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet) @@ -572,7 +646,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['option', 'default-router', router]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 280932fd7..8d9b8459e 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -23,6 +23,7 @@ from urllib3.exceptions import InsecureRequestWarning from base_vyostest_shim import VyOSUnitTestSHIM from base_vyostest_shim import ignore_warning from vyos.utils.file import read_file +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import process_named_running @@ -52,7 +53,22 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ +dh_1024 = """ +MIGHAoGBAM3nvMkHGi/xmRs8cYg4pcl5sAanxel9EM+1XobVhUViXw8JvlmSEVOj +n2aXUifc4SEs3WDzVPRC8O8qQWjvErpTq/HOgt3aqBCabMgvflmt706XP0KiqnpW +EyvNiI27J3wBUzEXLIS110MxPAX5Tcug974PecFcOxn1RWrbWcx/AgEC +""" + +dh_2048 = """ +MIIBCAKCAQEA1mld/V7WnxxRinkOlhx/BoZkRELtIUQFYxyARBqYk4C5G3YnZNNu +zjaGyPnfIKHu8SIUH85OecM+5/co9nYlcUJuph2tbR6qNgPw7LOKIhf27u7WhvJk +iVsJhwZiWmvvMV4jTParNEI2svoooMyhHXzeweYsg6YtgLVmwiwKj3XP3gRH2i3B +Mq8CDS7X6xaKvjfeMPZBFqOM5nb6HhsbaAUyiZxrfipLvXxtnbzd/eJUQVfVdxM3 +pn0i+QrO2tuNAzX7GoPc9pefrbb5xJmGS50G0uqsR59+7LhYmyZSBASA0lxTEW9t +kv/0LPvaYTY57WL7hBeqqHy/WPZHPzDI3wIBAg== +""" # to test load config via HTTP URL +nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest' nginx_conf_smoketest = """ server { listen 8000; @@ -81,6 +97,11 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) cls.cli_delete(cls, pki_base) + @classmethod + def tearDownClass(cls): + super(TestHTTPSService, cls).tearDownClass() + call(f'sudo rm -f {nginx_tmp_site}') + def tearDown(self): self.cli_delete(base_path) self.cli_delete(pki_base) @@ -89,33 +110,31 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) - def test_server_block(self): - vhost_id = 'example' - address = '0.0.0.0' - port = '8443' - name = 'example.org' - - test_path = base_path + ['virtual-host', vhost_id] - - self.cli_set(test_path + ['listen-address', address]) - self.cli_set(test_path + ['port', port]) - self.cli_set(test_path + ['server-name', name]) - - self.cli_commit() - - nginx_config = read_file('/etc/nginx/sites-enabled/default') - self.assertIn(f'listen {address}:{port} ssl;', nginx_config) - self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_certificate(self): - self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data.replace('\n','')]) - self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data.replace('\n','')]) - - self.cli_set(base_path + ['certificates', 'certificate', 'test_https']) + cert_name = 'test_https' + dh_name = 'dh-test' + + self.cli_set(base_path + ['certificates', 'certificate', cert_name]) + # verify() - certificates do not exist (yet) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')]) + self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')]) + + self.cli_set(base_path + ['certificates', 'dh-params', dh_name]) + # verify() - dh-params do not exist (yet) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')]) + # verify() - dh-param minimum length is 2048 bit + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')]) self.cli_commit() self.assertTrue(process_named_running(PROCESS_NAME)) + self.debug = False def test_api_missing_keys(self): self.cli_set(base_path + ['api']) @@ -135,15 +154,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): key = 'MySuperSecretVyOS' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) - test_path = base_path + ['virtual-host', vhost_id] - self.cli_set(test_path + ['listen-address', address]) - self.cli_set(test_path + ['server-name', name]) + self.cli_set(base_path + ['listen-address', address]) self.cli_commit() nginx_config = read_file('/etc/nginx/sites-enabled/default') self.assertIn(f'listen {address}:{port} ssl;', nginx_config) - self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) + self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default url = f'https://{address}/retrieve' payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'} @@ -402,19 +419,15 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): url_config = f'https://{address}/configure' headers = {} tmp_file = 'tmp-config.boot' - nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) self.cli_commit() # load config via HTTP requires nginx config call(f'sudo touch {nginx_tmp_site}') - call(f'sudo chown vyos:vyattacfg {nginx_tmp_site}') - call(f'sudo chmod +w {nginx_tmp_site}') - - with open(nginx_tmp_site, 'w') as f: - f.write(nginx_conf_smoketest) - call('sudo nginx -s reload') + call(f'sudo chmod 666 {nginx_tmp_site}') + write_file(nginx_tmp_site, nginx_conf_smoketest) + call('sudo systemctl reload nginx') # save config payload = { @@ -441,8 +454,8 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.assertEqual(r.status_code, 200) # cleanup tmp nginx conf - call(f'sudo rm -rf {nginx_tmp_site}') - call('sudo nginx -s reload') + call(f'sudo rm -f {nginx_tmp_site}') + call('sudo systemctl reload nginx') if __name__ == '__main__': unittest.main(verbosity=5) diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index 239e44c3b..4be40e99e 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -130,28 +130,27 @@ def get_config(config=None): if len(argv) > 1 and argv[1] == 'certbot_renew': pki['certbot_renew'] = {} - tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True) + tmp = node_changed(conf, base + ['ca'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'ca' : tmp}) - tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True) + tmp = node_changed(conf, base + ['certificate'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'certificate' : tmp}) - tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True) + tmp = node_changed(conf, base + ['dh'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'dh' : tmp}) - tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True) + tmp = node_changed(conf, base + ['key-pair'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'key_pair' : tmp}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), - recursive=True) + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True) if tmp: if 'changed' not in pki: pki.update({'changed':{}}) pki['changed'].update({'openvpn' : tmp}) diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index ce67ccff7..8d594bb68 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -220,7 +220,20 @@ def verify(isis): if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') - + + # Check for index ranges being larger than the segment routing global block + if dict_search('segment_routing.global_block', isis): + g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) + g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) + g_label_difference = int(g_high_label_value) - int(g_low_label_value) + if dict_search('segment_routing.prefix', isis): + for prefix, prefix_config in isis['segment_routing']['prefix'].items(): + if 'index' in prefix_config: + index_size = isis['segment_routing']['prefix'][prefix]['index']['value'] + if int(index_size) > int(g_label_difference): + raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ + f'index base size larger than the SRGB label base.') + # Check for LFA tiebreaker index duplication if dict_search('fast_reroute.lfa.local.tiebreaker', isis): comparison_dictionary = {} diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 2f07142a3..34cf49286 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -213,6 +213,19 @@ def verify(ospf): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') + # Check for index ranges being larger than the segment routing global block + if dict_search('segment_routing.global_block', ospf): + g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) + g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) + g_label_difference = int(g_high_label_value) - int(g_low_label_value) + if dict_search('segment_routing.prefix', ospf): + for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): + if 'index' in prefix_config: + index_size = ospf['segment_routing']['prefix'][prefix]['index']['value'] + if int(index_size) > int(g_label_difference): + raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ + f'index base size larger than the SRGB label base.') + # Check route summarisation if 'summary_address' in ospf: for prefix, prefix_options in ospf['summary_address'].items(): diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 7ebc560ba..ceaba019e 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -31,6 +31,7 @@ from vyos.utils.file import chmod_775 from vyos.utils.file import makedir from vyos.utils.file import write_file from vyos.utils.process import call +from vyos.utils.network import interface_exists from vyos.utils.network import is_subnet_connected from vyos.utils.network import is_addr_assigned from vyos import ConfigError @@ -222,6 +223,7 @@ def verify(dhcp): if 'static_mapping' in subnet_config: # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) + used_ips = [] for mapping, mapping_config in subnet_config['static_mapping'].items(): if 'ip_address' in mapping_config: if ip_address(mapping_config['ip_address']) not in ip_network(subnet): @@ -233,6 +235,11 @@ def verify(dhcp): raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') + if mapping_config['ip_address'] in used_ips: + raise ConfigError(f'Configured IP address for static mapping "{mapping}" exists on another static mapping') + + used_ips.append(mapping_config['ip_address']) + # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: @@ -294,12 +301,18 @@ def verify(dhcp): else: raise ConfigError(f'listen-address "{address}" not configured on any interface') - if not listen_ok: raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n' 'broadcast interface configured, nor was there an explicit listen-address\n' 'configured for serving DHCP relay packets!') + if 'listen_address' in dhcp and 'listen_interface' in dhcp: + raise ConfigError(f'Cannot define listen-address and listen-interface at the same time') + + for interface in (dict_search('listen_interface', dhcp) or []): + if not interface_exists(interface): + raise ConfigError(f'listen-interface "{interface}" does not exist') + return None def generate(dhcp): diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index 2e7ebda5a..46efc3c93 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -15,51 +15,41 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import socket import sys import json -from copy import deepcopy from time import sleep -import vyos.defaults - from vyos.base import Warning from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.defaults import api_config_state from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key +from vyos.pki import wrap_dh_parameters +from vyos.pki import load_dh_parameters from vyos.template import render +from vyos.utils.dict import dict_search from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_active from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file - +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/etc/nginx/sites-available/default' +config_file = '/etc/nginx/sites-enabled/default' systemd_override = r'/run/systemd/system/nginx.service.d/override.conf' -cert_dir = '/etc/ssl/certs' -key_dir = '/etc/ssl/private' - -api_config_state = '/run/http-api-state' -systemd_service = '/run/systemd/system/vyos-http-api.service' - -# https config needs to coordinate several subsystems: api, -# self-signed certificate, as well as the virtual hosts defined within the -# https config definition itself. Consequently, one needs a general dict, -# encompassing the https and other configs, and a list of such virtual hosts -# (server blocks in nginx terminology) to pass to the jinja2 template. -default_server_block = { - 'id' : '', - 'address' : '*', - 'port' : '443', - 'name' : ['_'], - 'api' : False, - 'vyos_cert' : {}, -} +cert_dir = '/run/nginx/certs' + +user = 'www-data' +group = 'www-data' + +systemd_service_api = '/run/systemd/system/vyos-http-api.service' def get_config(config=None): if config: @@ -71,83 +61,70 @@ def get_config(config=None): if not conf.exists(base): return None - diff = get_config_diff(conf) - - https = conf.get_config_dict(base, get_first_key=True, with_pki=True) + https = conf.get_config_dict(base, get_first_key=True, + key_mangling=('-', '_'), + with_pki=True) - https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) + # store path to API config file for later use in templates + https['api_config_state'] = api_config_state + # get fully qualified system hsotname + https['hostname'] = socket.getfqdn() - if 'api' not in https: - return https + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**https.kwargs, recursive=True) + if 'api' not in https or 'graphql' not in https['api']: + del default_values['api'] - http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - if http_api.from_defaults(['graphql']): - del http_api['graphql'] - - # Do we run inside a VRF context? - vrf_path = ['service', 'https', 'vrf'] - if conf.exists(vrf_path): - http_api['vrf'] = conf.return_value(vrf_path) - - https['api'] = http_api + # merge CLI and default dictionary + https = config_dict_merge(default_values, https) return https def verify(https): - from vyos.utils.dict import dict_search - if https is None: return None - if 'certificates' in https: - certificates = https['certificates'] + if 'certificates' in https and 'certificate' in https['certificates']: + cert_name = https['certificates']['certificate'] + if 'pki' not in https: + raise ConfigError('PKI is not configured!') - if 'certificate' in certificates: - if not https['pki']: - raise ConfigError('PKI is not configured') + if cert_name not in https['pki']['certificate']: + raise ConfigError('Invalid certificate in configuration!') - cert_name = certificates['certificate'] + pki_cert = https['pki']['certificate'][cert_name] - if cert_name not in https['pki']['certificate']: - raise ConfigError('Invalid certificate on https configuration') + if 'certificate' not in pki_cert: + raise ConfigError('Missing certificate in configuration!') - pki_cert = https['pki']['certificate'][cert_name] + if 'private' not in pki_cert or 'key' not in pki_cert['private']: + raise ConfigError('Missing certificate private key in configuration!') - if 'certificate' not in pki_cert: - raise ConfigError('Missing certificate on https configuration') + if 'dh_params' in https['certificates']: + dh_name = https['certificates']['dh_params'] + if dh_name not in https['pki']['dh']: + raise ConfigError('Invalid DH parameter in configuration!') - if 'private' not in pki_cert or 'key' not in pki_cert['private']: - raise ConfigError("Missing certificate private key on https configuration") - else: - Warning('No certificate specified, using buildin self-signed certificates!') + pki_dh = https['pki']['dh'][dh_name] + 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') - server_block_list = [] + else: + Warning('No certificate specified, using build-in self-signed certificates. '\ + 'Do not use them in a production environment!') - # organize by vhosts - vhost_dict = https.get('virtual-host', {}) + # Check if server port is already in use by a different appliaction + listen_address = ['0.0.0.0'] + port = int(https['port']) + if 'listen_address' in https: + listen_address = https['listen_address'] - if not vhost_dict: - # no specified virtual hosts (server blocks); use default - server_block_list.append(default_server_block) - else: - for vhost in list(vhost_dict): - server_block = deepcopy(default_server_block) - data = vhost_dict.get(vhost, {}) - server_block['address'] = data.get('listen-address', '*') - server_block['port'] = data.get('port', '443') - server_block_list.append(server_block) - - for entry in server_block_list: - _address = entry.get('address') - _address = '0.0.0.0' if _address == '*' else _address - _port = entry.get('port') - proto = 'tcp' - if check_port_availability(_address, int(_port), proto) is not True and \ - not is_listen_port_bind_service(int(_port), 'nginx'): - raise ConfigError(f'"{proto}" port "{_port}" is used by another service') + for address in listen_address: + if not check_port_availability(address, port, 'tcp') and not is_listen_port_bind_service(port, 'nginx'): + raise ConfigError(f'TCP port "{port}" is used by another service!') verify_vrf(https) @@ -172,89 +149,61 @@ def verify(https): # If only key-based methods are enabled, # fail the commit if no valid key configurations are found if (not valid_keys_exist) and (not jwt_auth): - raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled') + raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled!') if (not valid_keys_exist) and jwt_auth: - Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.') + Warning(f'API keys are not configured: classic (non-GraphQL) API will be unavailable!') return None def generate(https): if https is None: + for file in [systemd_service_api, config_file, systemd_override]: + if os.path.exists(file): + os.unlink(file) return None - if 'api' not in https: - if os.path.exists(systemd_service): - os.unlink(systemd_service) - else: - render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) + if 'api' in https: + render(systemd_service_api, 'https/vyos-http-api.service.j2', https) with open(api_config_state, 'w') as f: json.dump(https['api'], f, indent=2) - - server_block_list = [] - - # organize by vhosts - - vhost_dict = https.get('virtual-host', {}) - - if not vhost_dict: - # no specified virtual hosts (server blocks); use default - server_block_list.append(default_server_block) else: - for vhost in list(vhost_dict): - server_block = deepcopy(default_server_block) - server_block['id'] = vhost - data = vhost_dict.get(vhost, {}) - server_block['address'] = data.get('listen-address', '*') - server_block['port'] = data.get('port', '443') - name = data.get('server-name', ['_']) - server_block['name'] = name - allow_client = data.get('allow-client', {}) - server_block['allow_client'] = allow_client.get('address', []) - server_block_list.append(server_block) + if os.path.exists(systemd_service_api): + os.unlink(systemd_service_api) # get certificate data - - cert_dict = https.get('certificates', {}) - - if 'certificate' in cert_dict: - cert_name = cert_dict['certificate'] + if 'certificates' in https and 'certificate' in https['certificates']: + cert_name = https['certificates']['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') + cert_path = os.path.join(cert_dir, f'{cert_name}_cert.pem') + key_path = os.path.join(cert_dir, f'{cert_name}_key.pem') server_cert = str(wrap_certificate(pki_cert['certificate'])) - if 'ca-certificate' in cert_dict: - ca_cert = cert_dict['ca-certificate'] - server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) - write_file(cert_path, server_cert) - write_file(key_path, wrap_private_key(pki_cert['private']['key'])) + # Append CA certificate if specified to form a full chain + if 'ca_certificate' in https['certificates']: + ca_cert = https['certificates']['ca_certificate'] + server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) - vyos_cert_data = { - 'crt': cert_path, - 'key': key_path - } + write_file(cert_path, server_cert, user=user, group=group, mode=0o644) + write_file(key_path, wrap_private_key(pki_cert['private']['key']), + user=user, group=group, mode=0o600) - for block in server_block_list: - block['vyos_cert'] = vyos_cert_data + tmp_path = {'cert_path': cert_path, 'key_path': key_path} - if 'api' in list(https): - vhost_list = https.get('api-restrict', {}).get('virtual-host', []) - if not vhost_list: - for block in server_block_list: - block['api'] = True - else: - for block in server_block_list: - if block['id'] in vhost_list: - block['api'] = True + if 'dh_params' in https['certificates']: + dh_name = https['certificates']['dh_params'] + pki_dh = https['pki']['dh'][dh_name] + if 'parameters' in pki_dh: + dh_path = os.path.join(cert_dir, f'{dh_name}_dh.pem') + write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']), + user=user, group=group, mode=0o600) + tmp_path.update({'dh_file' : dh_path}) - data = { - 'server_block_list': server_block_list, - } + https['certificates'].update(tmp_path) - render(config_file, 'https/nginx.default.j2', data) + render(config_file, 'https/nginx.default.j2', https) render(systemd_override, 'https/override.conf.j2', https) return None @@ -273,7 +222,7 @@ def apply(https): call(f'systemctl reload-or-restart {http_api_service_name}') # Let uvicorn settle before (possibly) restarting nginx sleep(1) - else: + elif is_systemd_service_active(http_api_service_name): call(f'systemctl stop {http_api_service_name}') call(f'systemctl reload-or-restart {https_service_name}') diff --git a/src/etc/systemd/system/nginx.service.d/10-override.conf b/src/etc/systemd/system/nginx.service.d/10-override.conf new file mode 100644 index 000000000..1be5cec81 --- /dev/null +++ b/src/etc/systemd/system/nginx.service.d/10-override.conf @@ -0,0 +1,3 @@ +[Unit] +After= +After=vyos-router.service diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 01b06526d..42de696ce 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -102,7 +102,8 @@ def failsafe(config_file_name): 'authentication', 'encrypted-password']) - cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos") + cmd(f"useradd --create-home --no-user-group --shell /bin/vbash --password '{passwd}' "\ + "--groups frr,frrvty,vyattacfg,sudo,adm,dip,disk vyos") if __name__ == '__main__': if len(sys.argv) < 2: diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9 new file mode 100755 index 000000000..908420c18 --- /dev/null +++ b/src/migration-scripts/dhcp-server/8-to-9 @@ -0,0 +1,69 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T3316: +# - Migrate dhcp options under new option node + +import sys +import re +from vyos.configtree import ConfigTree + +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() + +base = ['service', 'dhcp-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal', + 'client-prefix-length', 'default-router', 'domain-name', 'domain-search', + 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server', + 'pop-server', 'server-identifier', 'smtp-server', 'static-route', + 'tftp-server-name', 'time-offset', 'time-server', 'time-zone', + 'vendor-option', 'wins-server', 'wpad-url'] + +for network in config.list_nodes(base): + for option in option_nodes: + if config.exists(base + [network, option]): + config.set(base + [network, 'option']) + config.copy(base + [network, option], base + [network, 'option', option]) + config.delete(base + [network, option]) + + if config.exists(base + [network, 'subnet']): + for subnet in config.list_nodes(base + [network, 'subnet']): + base_subnet = base + [network, 'subnet', subnet] + + for option in option_nodes: + if config.exists(base + [network, 'subnet', subnet, option]): + config.set(base + [network, 'subnet', subnet, 'option']) + config.copy(base + [network, 'subnet', subnet, option], base + [network, 'subnet', subnet, 'option', option]) + config.delete(base + [network, 'subnet', subnet, option]) + +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/https/5-to-6 b/src/migration-scripts/https/5-to-6 index b4159f02f..6d6efd32c 100755 --- a/src/migration-scripts/https/5-to-6 +++ b/src/migration-scripts/https/5-to-6 @@ -16,12 +16,14 @@ # T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot # to new "pki certificate" CLI tree +# T5902: Remove virtual-host import os import sys from vyos.configtree import ConfigTree from vyos.defaults import directories +from vyos.utils.process import cmd vyos_certbot_dir = directories['certbot'] @@ -36,30 +38,68 @@ with open(file_name, 'r') as f: config = ConfigTree(config_file) -base = ['service', 'https', 'certificates'] +base = ['service', 'https'] if not config.exists(base): # Nothing to do sys.exit(0) -# both domain-name and email must be set on CLI - ensured by previous verify() -domain_names = config.return_values(base + ['certbot', 'domain-name']) -email = config.return_value(base + ['certbot', 'email']) -config.delete(base) - -# Set default certname based on domain-name -cert_name = 'https-' + domain_names[0].split('.')[0] -# Overwrite certname from previous certbot calls if available -if os.path.exists(f'{vyos_certbot_dir}/live'): - for cert in [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()]: - cert_name = cert - break - -for domain in domain_names: - config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) +if config.exists(base + ['certificates']): + # both domain-name and email must be set on CLI - ensured by previous verify() + domain_names = config.return_values(base + ['certificates', 'certbot', 'domain-name']) + email = config.return_value(base + ['certificates', 'certbot', 'email']) + config.delete(base + ['certificates']) + + # Set default certname based on domain-name + cert_name = 'https-' + domain_names[0].split('.')[0] + # Overwrite certname from previous certbot calls if available + # We can not use python code like os.scandir due to filesystem permissions. + # This must be run as root + certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing / + if os.path.exists(certbot_live): + tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d') + tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net'] + tmp.remove(certbot_live) + cert_name = tmp[0].replace(certbot_live, '') + config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email) + config.set_tag(['pki', 'certificate']) + for domain in domain_names: + config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) + + # Update Webserver certificate + config.set(base + ['certificates', 'certificate'], value=cert_name) + +if config.exists(base + ['virtual-host']): + allow_client = [] + listen_port = [] + listen_address = [] + for virtual_host in config.list_nodes(base + ['virtual-host']): + allow_path = base + ['virtual-host', virtual_host, 'allow-client', 'address'] + if config.exists(allow_path): + tmp = config.return_values(allow_path) + allow_client.extend(tmp) + + port_path = base + ['virtual-host', virtual_host, 'listen-port'] + if config.exists(port_path): + tmp = config.return_value(port_path) + listen_port.append(tmp) + + listen_address_path = base + ['virtual-host', virtual_host, 'listen-address'] + if config.exists(listen_address_path): + tmp = config.return_value(listen_address_path) + listen_address.append(tmp) + + config.delete(base + ['virtual-host']) + for client in allow_client: + config.set(base + ['allow-client', 'address'], value=client, replace=False) + + # clear listen-address if "all" were specified + if '*' in listen_address: + listen_address = [] + for address in listen_address: + config.set(base + ['listen-address'], value=address, replace=False) + -# Update Webserver certificate -config.set(base + ['certificate'], value=cert_name) try: with open(file_name, 'w') as f: diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index e75485f9f..e64a85b95 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:' MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first' MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +def annotated_list(images_list: list[str]) -> list[str]: + """Annotate list of images with additional info + + Args: + images_list (list[str]): a list of image names + + Returns: + list[str]: a list of image names with additional info + """ + index_running: int = None + index_default: int = None + try: + index_running = images_list.index(image.get_running_image()) + index_default = images_list.index(image.get_default_image()) + except ValueError: + pass + if index_running is not None: + images_list[index_running] += ' (running)' + if index_default is not None: + images_list[index_default] += ' (default boot)' + return images_list @compat.grub_cfg_update def delete_image(image_name: Optional[str] = None, @@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None, Args: image_name (str): a name of image to delete """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if no_prompt: exit('An image name is required for delete action') @@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None, Args: image_name (str): an image name """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if not prompt: exit('An image name is required for set action') diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py new file mode 100644 index 000000000..d24b1065b --- /dev/null +++ b/src/op_mode/zone.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import typing +import sys +import vyos.opmode + +import tabulate +from vyos.configquery import ConfigTreeQuery +from vyos.utils.dict import dict_search_args +from vyos.utils.dict import dict_search + + +def get_config_zone(conf, name=None): + config_path = ['firewall', 'zone'] + if name: + config_path += [name] + + zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return zone_policy + + +def _convert_one_zone_data(zone: str, zone_config: dict) -> dict: + """ + Convert config dictionary of one zone to API dictionary + :param zone: Zone name + :type zone: str + :param zone_config: config dictionary + :type zone_config: dict + :return: AP dictionary + :rtype: dict + """ + list_of_rules = [] + intrazone_dict = {} + if dict_search('from', zone_config): + for from_zone, from_zone_config in zone_config['from'].items(): + from_zone_dict = {'name': from_zone} + if dict_search('firewall.name', from_zone_config): + from_zone_dict['firewall'] = dict_search('firewall.name', + from_zone_config) + if dict_search('firewall.ipv6_name', from_zone_config): + from_zone_dict['firewall_v6'] = dict_search( + 'firewall.ipv6_name', from_zone_config) + list_of_rules.append(from_zone_dict) + + zone_dict = { + 'name': zone, + 'interface': dict_search('interface', zone_config), + 'type': 'LOCAL' if dict_search('local_zone', + zone_config) is not None else None, + } + if list_of_rules: + zone_dict['from'] = list_of_rules + if dict_search('intra_zone_filtering.firewall.name', zone_config): + intrazone_dict['firewall'] = dict_search( + 'intra_zone_filtering.firewall.name', zone_config) + if dict_search('intra_zone_filtering.firewall.ipv6_name', zone_config): + intrazone_dict['firewall_v6'] = dict_search( + 'intra_zone_filtering.firewall.ipv6_name', zone_config) + if intrazone_dict: + zone_dict['intrazone'] = intrazone_dict + return zone_dict + + +def _convert_zones_data(zone_policies: dict) -> list: + """ + Convert all config dictionary to API list of zone dictionaries + :param zone_policies: config dictionary + :type zone_policies: dict + :return: API list + :rtype: list + """ + zone_list = [] + for zone, zone_config in zone_policies.items(): + zone_list.append(_convert_one_zone_data(zone, zone_config)) + return zone_list + + +def _convert_config(zones_config: dict, zone: str = None) -> list: + """ + convert config to API list + :param zones_config: zones config + :type zones_config: + :param zone: zone name + :type zone: str + :return: API list + :rtype: list + """ + if zone: + if zones_config: + output = [_convert_one_zone_data(zone, zones_config)] + else: + raise vyos.opmode.DataUnavailable(f'Zone {zone} not found') + else: + if zones_config: + output = _convert_zones_data(zones_config) + else: + raise vyos.opmode.UnconfiguredSubsystem( + 'Zone entries are not configured') + return output + + +def output_zone_list(zone_conf: dict) -> list: + """ + Format one zone row + :param zone_conf: zone config + :type zone_conf: dict + :return: formatted list of zones + :rtype: list + """ + zone_info = [zone_conf['name']] + if zone_conf['type'] == 'LOCAL': + zone_info.append('LOCAL') + else: + zone_info.append("\n".join(zone_conf['interface'])) + + from_zone = [] + firewall = [] + firewall_v6 = [] + if 'intrazone' in zone_conf: + from_zone.append(zone_conf['name']) + + v4_name = dict_search_args(zone_conf['intrazone'], 'firewall') + v6_name = dict_search_args(zone_conf['intrazone'], 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + if 'from' in zone_conf: + for from_conf in zone_conf['from']: + from_zone.append(from_conf['name']) + + v4_name = dict_search_args(from_conf, 'firewall') + v6_name = dict_search_args(from_conf, 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + zone_info.append("\n".join(from_zone)) + zone_info.append("\n".join(firewall)) + zone_info.append("\n".join(firewall_v6)) + return zone_info + + +def get_formatted_output(zone_policy: list) -> str: + """ + Formatted output of all zones + :param zone_policy: list of zones + :type zone_policy: list + :return: formatted table with zones + :rtype: str + """ + headers = ["Zone", + "Interfaces", + "From Zone", + "Firewall IPv4", + "Firewall IPv6" + ] + formatted_list = [] + for zone_conf in zone_policy: + formatted_list.append(output_zone_list(zone_conf)) + tabulate.PRESERVE_WHITESPACE = True + output = tabulate.tabulate(formatted_list, headers, numalign="left") + return output + + +def show(raw: bool, zone: typing.Optional[str]): + """ + Show zone-policy command + :param raw: if API + :type raw: bool + :param zone: zone name + :type zone: str + """ + conf: ConfigTreeQuery = ConfigTreeQuery() + zones_config: dict = get_config_zone(conf, zone) + zone_policy_api: list = _convert_config(zones_config, zone) + if raw: + return zone_policy_api + else: + return get_formatted_output(zone_policy_api) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1)
\ No newline at end of file diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index b64e58132..40d442e30 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -1,6 +1,6 @@ #!/usr/share/vyos-http-api-tools/bin/python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,8 +13,6 @@ # # 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 @@ -25,6 +23,7 @@ import logging import signal import traceback import threading + from time import sleep from typing import List, Union, Callable, Dict @@ -46,11 +45,12 @@ from ariadne.asgi import GraphQL from vyos.config import Config from vyos.configtree import ConfigTree from vyos.configdiff import get_config_diff -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.defaults import api_config_state import api.graphql.state -api_config_state = '/run/http-api-state' CFG_GROUP = 'vyattacfg' debug = True diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 03574bdc3..e1a9f1884 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -15,28 +15,71 @@ if [ $# -lt 1 ]; then fi action=$1 -client_name=$LEASE4_HOSTNAME -client_ip=$LEASE4_ADDRESS -client_mac=$LEASE4_HWADDR hostsd_client="/usr/bin/vyos-hostsd-client" -case "$action" in - lease4_renew|lease4_recover) # add mapping for new/recovered lease address - if [ -z "$client_name" ]; then - logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" - client_name=$(echo "host-$client_mac" | tr : -) - fi +get_subnet_domain_name () { + python3 <<EOF +from vyos.kea import kea_get_active_config +from vyos.utils.dict import dict_search_args + +config = kea_get_active_config('4') +shared_networks = dict_search_args(config, 'arguments', f'Dhcp4', 'shared-networks') + +found = False - $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply +if shared_networks: + for network in shared_networks: + for subnet in network[f'subnet4']: + if subnet['id'] == $1: + for option in subnet['option-data']: + if option['name'] == 'domain-name': + print(option['data']) + found = True + + if not found: + for option in network['option-data']: + if option['name'] == 'domain-name': + print(option['data']) +EOF +} + +case "$action" in + lease4_renew|lease4_recover) exit 0 ;; lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address + client_ip=$LEASE4_ADDRESS $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply exit 0 ;; - leases4_committed) # nothing to do + leases4_committed) # process committed leases (added/renewed/recovered) + for ((i = 0; i < $LEASES4_SIZE; i++)); do + client_ip_var="LEASES4_AT${i}_ADDRESS" + client_mac_var="LEASES4_AT${i}_HWADDR" + client_name_var="LEASES4_AT${i}_HOSTNAME" + client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID" + + client_ip=${!client_ip_var} + client_mac=${!client_mac_var} + client_name=${!client_name_var} + client_subnet_id=${!client_subnet_id_var} + + if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "host-$client_mac" | tr : -) + fi + + client_domain=$(get_subnet_domain_name $client_subnet_id) + + if [ -n "$client_domain" ]; then + client_name="$client_name.$client_domain" + fi + + $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply + done + exit 0 ;; |