diff options
73 files changed, 1112 insertions, 654 deletions
diff --git a/.github/workflows/pr-conflicts.yml b/.github/workflows/pr-conflicts.yml index 72ff3969b..96040cd60 100644 --- a/.github/workflows/pr-conflicts.yml +++ b/.github/workflows/pr-conflicts.yml @@ -6,7 +6,7 @@ on: jobs: Conflict_Check: name: 'Check PR status: conflicts and resolution' - runs-on: ubuntu-18.04 + runs-on: ubuntu-22.04 steps: - name: check if PRs are dirty uses: eps1lon/actions-label-merge-conflict@releases/2.x diff --git a/.gitignore b/.gitignore index a31d037f2..fe92f5b9d 100644 --- a/.gitignore +++ b/.gitignore @@ -138,3 +138,5 @@ debian/*.substvars # vyos-1x JSON version data/component-versions.json +# vyos-1x XML cache +python/vyos/xml_ref/cache.py diff --git a/data/configd-include.json b/data/configd-include.json index 456211caa..e8f090c46 100644 --- a/data/configd-include.json +++ b/data/configd-include.json @@ -9,7 +9,7 @@ "dhcpv6_relay.py", "dhcpv6_server.py", "dns_forwarding.py", -"dynamic_dns.py", +"dns_dynamic.py", "firewall.py", "flow_accounting_conf.py", "high-availability.py", diff --git a/data/templates/container/storage.conf.j2 b/data/templates/container/storage.conf.j2 index ec2046fb5..1a4e601b5 100644 --- a/data/templates/container/storage.conf.j2 +++ b/data/templates/container/storage.conf.j2 @@ -2,5 +2,6 @@ [storage] driver = "overlay" graphroot = "/usr/lib/live/mount/persistence/container/storage" + runroot = "/var/run/containers/storage" [storage.options] mount_program = "/usr/bin/fuse-overlayfs" diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 new file mode 100644 index 000000000..4da7153c7 --- /dev/null +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -0,0 +1,75 @@ +{% macro render_config(host, address, web_options, ip_suffixes=['']) %} +{# Address: use=if, if=ethX, usev6=ifv6, ifv6=ethX, usev6=webv6, webv6=https://v6.example.com #} +{% for ipv in ip_suffixes %} +use{{ ipv }}={{ address if address == 'web' else 'if' }}{{ ipv }}, \ +{% if address == 'web' %} +{% if web_options.url is vyos_defined %} +web{{ ipv }}={{ web_options.url }}, \ +{% endif %} +{% if web_options.skip is vyos_defined %} +web-skip{{ ipv }}='{{ web_options.skip }}', \ +{% endif %} +{% else %} +if{{ ipv }}={{ address }}, \ +{% endif %} +{% endfor %} +{# Other service options #} +{% for k,v in kwargs.items() %} +{% if v is vyos_defined %} +{{ k }}={{ v }}{{ ',' if not loop.last }} \ +{% endif %} +{% endfor %} +{# Actual hostname for the service #} +{{ host }} +{% endmacro %} +### Autogenerated by dns_dynamic.py ### +daemon=1m +syslog=yes +ssl=yes +pid={{ config_file | replace('.conf', '.pid') }} +cache={{ config_file | replace('.conf', '.cache') }} +{# Explicitly override global options for reliability #} +web=googledomains {# ddclient default ('dyndns') doesn't support ssl and results in process lockup #} +use=no {# ddclient default ('ip') results in confusing warning message in log #} + +{% if address is vyos_defined %} +{% for address, service_cfg in address.items() %} +{% if service_cfg.rfc2136 is vyos_defined %} +{% for name, config in service_cfg.rfc2136.items() %} +{% if config.description is vyos_defined %} +# {{ config.description }} + +{% endif %} +{% for host in config.host_name if config.host_name is vyos_defined %} +# RFC2136 dynamic DNS configuration for {{ name }}: [{{ config.zone }}, {{ host }}] +{# Don't append 'new-style' compliant suffix ('usev4', 'usev6', 'ifv4', 'ifv6' etc.) + to the properties since 'nsupdate' doesn't support that yet. #} +{{ render_config(host, address, service_cfg.web_options, + protocol='nsupdate', server=config.server, zone=config.zone, + password=config.key, ttl=config.ttl) }} + +{% endfor %} +{% endfor %} +{% endif %} +{% if service_cfg.service is vyos_defined %} +{% for name, config in service_cfg.service.items() %} +{% if config.description is vyos_defined %} +# {{ config.description }} + +{% endif %} +{% for host in config.host_name if config.host_name is vyos_defined %} +{% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' + else (['v6'] if config.ip_version == 'ipv6' else ['']) %} +# Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] +{# For ipv4 only setup or legacy ipv6 setup, don't append 'new-style' compliant suffix + ('usev4', 'ifv4', 'webv4' etc.) to the properties and instead live through the + deprecation warnings for better compatibility with most ddclient protocols. #} +{{ render_config(host, address, service_cfg.web_options, ip_suffixes, + protocol=config.protocol, server=config.server, zone=config.zone, + login=config.username, password=config.password) }} + +{% endfor %} +{% endfor %} +{% endif %} +{% endfor %} +{% endif %} diff --git a/data/templates/dns-dynamic/override.conf.j2 b/data/templates/dns-dynamic/override.conf.j2 new file mode 100644 index 000000000..6ca1b8a45 --- /dev/null +++ b/data/templates/dns-dynamic/override.conf.j2 @@ -0,0 +1,10 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} +[Unit] +ConditionPathExists={{ config_file }} +After=vyos-router.service + +[Service] +PIDFile={{ config_file | replace('.conf', '.pid') }} +EnvironmentFile= +ExecStart= +ExecStart=/usr/bin/ddclient -file {{ config_file }} diff --git a/data/templates/dynamic-dns/ddclient.conf.j2 b/data/templates/dynamic-dns/ddclient.conf.j2 deleted file mode 100644 index e8ef5ac90..000000000 --- a/data/templates/dynamic-dns/ddclient.conf.j2 +++ /dev/null @@ -1,53 +0,0 @@ -### Autogenerated by dynamic_dns.py ### -daemon=1m -syslog=yes -ssl=yes - -{% if interface is vyos_defined %} -{% for iface, iface_config in interface.items() %} -# ddclient configuration for interface "{{ iface }}" -{% if iface_config.use_web is vyos_defined %} -{% set web_skip = ", web-skip='" ~ iface_config.use_web.skip ~ "'" if iface_config.use_web.skip is vyos_defined else '' %} -use=web, web='{{ iface_config.use_web.url }}'{{ web_skip }} -{% else %} -{{ 'usev6=ifv6' if iface_config.ipv6_enable is vyos_defined else 'use=if' }}, if={{ iface }} -{% endif %} - -{% if iface_config.rfc2136 is vyos_defined %} -{% for rfc2136, config in iface_config.rfc2136.items() %} -{% for dns_record in config.record if config.record is vyos_defined %} -# RFC2136 dynamic DNS configuration for {{ rfc2136 }}, {{ config.zone }}, {{ dns_record }} -server={{ config.server }} -protocol=nsupdate -password={{ config.key }} -ttl={{ config.ttl }} -zone={{ config.zone }} -{{ dns_record }} - -{% endfor %} -{% endfor %} -{% endif %} - -{% if iface_config.service is vyos_defined %} -{% for service, config in iface_config.service.items() %} -{% for dns_record in config.host_name %} -# DynDNS provider configuration for {{ service }}, {{ dns_record }} -protocol={{ config.protocol }}, -max-interval=28d, -{% if config.login is vyos_defined %} -login={{ config.login }}, -{% endif %} -password='{{ config.password }}', -{% if config.server is vyos_defined %} -server={{ config.server }}, -{% endif %} -{% if config.zone is vyos_defined %} -zone={{ config.zone }}, -{% endif %} -{{ dns_record }} - -{% endfor %} -{% endfor %} -{% endif %} -{% endfor %} -{% endif %} diff --git a/data/templates/mdns-repeater/avahi-daemon.j2 b/data/templates/mdns-repeater/avahi-daemon.j2 index 3aaa7fc82..e0dfd897e 100644 --- a/data/templates/mdns-repeater/avahi-daemon.j2 +++ b/data/templates/mdns-repeater/avahi-daemon.j2 @@ -1,3 +1,4 @@ +### Autogenerated by service_mdns-repeater.py ### [server] use-ipv4=yes use-ipv6=yes diff --git a/data/templates/pmacct/uacctd.conf.j2 b/data/templates/pmacct/uacctd.conf.j2 index 8fbc09e83..1370f8121 100644 --- a/data/templates/pmacct/uacctd.conf.j2 +++ b/data/templates/pmacct/uacctd.conf.j2 @@ -53,7 +53,7 @@ nfprobe_maxflows[{{ nf_server_key }}]: {{ netflow.max_flows }} sampling_rate[{{ nf_server_key }}]: {{ netflow.sampling_rate }} {% endif %} {% if netflow.source_address is vyos_defined %} -nfprobe_source_ip[{{ nf_server_key }}]: {{ netflow.source_address }} +nfprobe_source_ip[{{ nf_server_key }}]: {{ netflow.source_address | bracketize_ipv6 }} {% endif %} {% if netflow.timeout is vyos_defined %} nfprobe_timeouts[{{ nf_server_key }}]: expint={{ netflow.timeout.expiry_interval }}:general={{ netflow.timeout.flow_generic }}:icmp={{ netflow.timeout.icmp }}:maxlife={{ netflow.timeout.max_active_life }}:tcp.fin={{ netflow.timeout.tcp_fin }}:tcp={{ netflow.timeout.tcp_generic }}:tcp.rst={{ netflow.timeout.tcp_rst }}:udp={{ netflow.timeout.udp }} @@ -73,7 +73,7 @@ sfprobe_agentip[{{ sf_server_key }}]: {{ sflow.agent_address }} sampling_rate[{{ sf_server_key }}]: {{ sflow.sampling_rate }} {% endif %} {% if sflow.source_address is vyos_defined %} -sfprobe_source_ip[{{ sf_server_key }}]: {{ sflow.source_address }} +sfprobe_source_ip[{{ sf_server_key }}]: {{ sflow.source_address | bracketize_ipv6 }} {% endif %} {% endfor %} diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install index 2b04f173b..07cdf8c74 100644 --- a/debian/vyos-1x.install +++ b/debian/vyos-1x.install @@ -9,6 +9,7 @@ etc/ppp etc/rsyslog.conf etc/securetty etc/security +etc/skel etc/sudoers.d etc/systemd etc/sysctl.d diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst index 58f24cb5a..949ffcbc4 100644 --- a/debian/vyos-1x.preinst +++ b/debian/vyos-1x.preinst @@ -1,6 +1,8 @@ -dpkg-divert --package vyos-1x --add --rename /etc/securetty -dpkg-divert --package vyos-1x --add --rename /etc/security/capability.conf -dpkg-divert --package vyos-1x --add --rename /lib/systemd/system/lcdproc.service -dpkg-divert --package vyos-1x --add --rename /etc/logrotate.d/conntrackd -dpkg-divert --package vyos-1x --add --rename /usr/share/pam-configs/radius -dpkg-divert --package vyos-1x --add --rename /etc/rsyslog.conf +dpkg-divert --package vyos-1x --add --no-rename /etc/securetty +dpkg-divert --package vyos-1x --add --no-rename /etc/security/capability.conf +dpkg-divert --package vyos-1x --add --no-rename /lib/systemd/system/lcdproc.service +dpkg-divert --package vyos-1x --add --no-rename /etc/logrotate.d/conntrackd +dpkg-divert --package vyos-1x --add --no-rename /usr/share/pam-configs/radius +dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.conf +dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.bashrc +dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.profile diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 48c101d73..c7b45b8f7 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -4,149 +4,102 @@ <children> <node name="dns"> <properties> - <help>Domain Name System related services</help> + <help>Domain Name System (DNS) related services</help> </properties> <children> - <node name="dynamic" owner="${vyos_conf_scripts_dir}/dynamic_dns.py"> + <node name="dynamic" owner="${vyos_conf_scripts_dir}/dns_dynamic.py"> <properties> <help>Dynamic DNS</help> </properties> <children> - <tagNode name="interface"> + <tagNode name="address"> <properties> - <help>Interface to send Dynamic DNS updates for</help> - <completionHelp> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> + <help>Obtain IP address to send Dynamic DNS update for</help> <valueHelp> <format>txt</format> - <description>Interface name</description> + <description>Use interface to obtain the IP address</description> </valueHelp> + <valueHelp> + <format>web</format> + <description>Use HTTP(S) web request to obtain the IP address</description> + </valueHelp> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + <list>web</list> + </completionHelp> <constraint> #include <include/constraint/interface-name.xml.i> + <regex>web</regex> </constraint> </properties> <children> - <tagNode name="rfc2136"> + <node name="web-options"> <properties> - <help>RFC2136 Update name</help> + <help>Options when using HTTP(S) web request to obtain the IP address</help> </properties> <children> - <leafNode name="key"> + #include <include/url.xml.i> + <leafNode name="skip"> <properties> - <help>File containing the secret key shared with remote DNS server</help> + <help>Pattern to skip from the HTTP(S) respose</help> <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> + <format>txt</format> + <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> </valueHelp> </properties> </leafNode> - <leafNode name="record"> - <properties> - <help>Record to be updated</help> - <multi/> - </properties> - </leafNode> - <leafNode name="server"> - <properties> - <help>Server to be updated</help> - </properties> - </leafNode> - <leafNode name="ttl"> + </children> + </node> + <tagNode name="rfc2136"> + <properties> + <help>RFC2136 nsupdate configuration</help> + <valueHelp> + <format>txt</format> + <description>RFC2136 nsupdate service name</description> + </valueHelp> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/dns/dynamic-service-host-name-server.xml.i> + <leafNode name="key"> <properties> - <help>Time To Live (default: 600)</help> + <help>File containing the TSIG secret key shared with remote DNS server</help> <valueHelp> - <format>u32:1-86400</format> - <description>DNS forwarding cache size</description> + <format>filename</format> + <description>File in /config/auth directory</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-86400"/> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> </constraint> </properties> - <defaultValue>600</defaultValue> </leafNode> + #include <include/dns/time-to-live.xml.i> <leafNode name="zone"> <properties> - <help>Zone to be updated</help> + <help>Forwarding zone to be updated</help> + <valueHelp> + <format>txt</format> + <description>RFC2136 Zone to be updated</description> + </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> </properties> </leafNode> </children> </tagNode> <tagNode name="service"> <properties> - <help>Service being used for Dynamic DNS</help> - <completionHelp> - <list>afraid changeip cloudflare dnspark dslreports dyndns easydns namecheap noip sitelutions zoneedit</list> - </completionHelp> + <help>Dynamic DNS configuration</help> <valueHelp> <format>txt</format> - <description>Dynanmic DNS service with a custom name</description> - </valueHelp> - <valueHelp> - <format>afraid</format> - <description>afraid.org Services</description> - </valueHelp> - <valueHelp> - <format>changeip</format> - <description>changeip.com Services</description> - </valueHelp> - <valueHelp> - <format>cloudflare</format> - <description>cloudflare.com Services</description> + <description>Dynamic DNS service name</description> </valueHelp> - <valueHelp> - <format>dnspark</format> - <description>dnspark.com Services</description> - </valueHelp> - <valueHelp> - <format>dslreports</format> - <description>dslreports.com Services</description> - </valueHelp> - <valueHelp> - <format>dyndns</format> - <description>dyndns.com Services</description> - </valueHelp> - <valueHelp> - <format>easydns</format> - <description>easydns.com Services</description> - </valueHelp> - <valueHelp> - <format>namecheap</format> - <description>namecheap.com Services</description> - </valueHelp> - <valueHelp> - <format>noip</format> - <description>noip.com Services</description> - </valueHelp> - <valueHelp> - <format>sitelutions</format> - <description>sitelutions.com Services</description> - </valueHelp> - <valueHelp> - <format>zoneedit</format> - <description>zoneedit.com Services</description> - </valueHelp> - <constraint> - <regex>(custom|afraid|changeip|cloudflare|dnspark|dslreports|dyndns|easydns|namecheap|noip|sitelutions|zoneedit|\w+)</regex> - </constraint> - <constraintErrorMessage>You can use only predefined list of services or word characters (_, a-z, A-Z, 0-9) as service name</constraintErrorMessage> </properties> <children> - <leafNode name="host-name"> - <properties> - <help>Hostname to register with Dynamic DNS service</help> - <constraint> - #include <include/constraint/host-name.xml.i> - </constraint> - <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage> - <multi/> - </properties> - </leafNode> - <leafNode name="login"> - <properties> - <help>Login/Username for Dynamic DNS service</help> - </properties> - </leafNode> + #include <include/generic-description.xml.i> + #include <include/dns/dynamic-service-host-name-server.xml.i> + #include <include/generic-username.xml.i> #include <include/generic-password.xml.i> <leafNode name="protocol"> <properties> @@ -159,7 +112,6 @@ </constraint> </properties> </leafNode> - #include <include/server-ipv4-fqdn.xml.i> <leafNode name="zone"> <properties> <help>DNS zone to update (not used by all protocols)</help> @@ -169,31 +121,33 @@ </valueHelp> </properties> </leafNode> - </children> - </tagNode> - <node name="use-web"> - <properties> - <help>Use HTTP(S) web request to obtain external IP address instead of the IP address associated with the interface</help> - </properties> - <children> - <leafNode name="skip"> + <leafNode name="ip-version"> <properties> - <help>Pattern to skip from the respose</help> + <help>IP address version to use</help> <valueHelp> - <format>txt</format> - <description>Pattern to skip from the respose of the given URL to extract the external IP address</description> + <format>_ipv4</format> + <description>Use only IPv4 address</description> + </valueHelp> + <valueHelp> + <format>_ipv6</format> + <description>Use only IPv6 address</description> </valueHelp> + <valueHelp> + <format>both</format> + <description>Use both IPv4 and IPv6 address</description> + </valueHelp> + <completionHelp> + <list>ipv4 ipv6 both</list> + </completionHelp> + <constraint> + <regex>(ipv[46]|both)</regex> + </constraint> + <constraintErrorMessage>IP Version must be literal 'ipv4', 'ipv6' or 'both'</constraintErrorMessage> </properties> + <defaultValue>ipv4</defaultValue> </leafNode> - #include <include/url.xml.i> </children> - </node> - <leafNode name="ipv6-enable"> - <properties> - <help>Explicitly use IPv6 address instead of IPv4 address to update the Dynamic DNS IP address</help> - <valueless/> - </properties> - </leafNode> + </tagNode> </children> </tagNode> </children> diff --git a/interface-definitions/dns-forwarding.xml.in b/interface-definitions/dns-forwarding.xml.in index ced1c9c31..86dc47a47 100644 --- a/interface-definitions/dns-forwarding.xml.in +++ b/interface-definitions/dns-forwarding.xml.in @@ -5,7 +5,7 @@ <children> <node name="dns"> <properties> - <help>Domain Name System related services</help> + <help>Domain Name System (DNS) related services</help> </properties> <children> <node name="forwarding" owner="${vyos_conf_scripts_dir}/dns_forwarding.py"> diff --git a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i new file mode 100644 index 000000000..ee1af2a36 --- /dev/null +++ b/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i @@ -0,0 +1,34 @@ +<!-- include start from dns/dynamic-service-host-name-server.xml.i --> +<leafNode name="host-name"> + <properties> + <help>Hostname to register with Dynamic DNS service</help> + <constraint> + #include <include/constraint/host-name.xml.i> + </constraint> + <constraintErrorMessage>Host-name must be alphanumeric and can contain hyphens</constraintErrorMessage> + <multi/> + </properties> +</leafNode> +<leafNode name="server"> + <properties> + <help>Remote Dynamic DNS server to send updates to</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of the remote server</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of the remote server</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Fully qualified domain name of the remote server</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Remote server must be IP address or fully qualified domain name</constraintErrorMessage> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/interface/netns.xml.i b/interface-definitions/include/interface/netns.xml.i index 39f9118fa..fd6da8f37 100644 --- a/interface-definitions/include/interface/netns.xml.i +++ b/interface-definitions/include/interface/netns.xml.i @@ -3,7 +3,7 @@ <properties> <help>Network namespace name</help> <valueHelp> - <format>text</format> + <format>txt</format> <description>Network namespace name</description> </valueHelp> <completionHelp> diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i new file mode 100644 index 000000000..b25fc6e76 --- /dev/null +++ b/interface-definitions/include/version/dns-dynamic-version.xml.i @@ -0,0 +1,3 @@ +<!-- include start from include/version/dns-dynamic-version.xml.i --> +<syntaxVersion component='dns-dynamic' version='1'></syntaxVersion> +<!-- include end --> diff --git a/interface-definitions/interfaces-geneve.xml.in b/interface-definitions/interfaces-geneve.xml.in index ac9794870..330dadd95 100644 --- a/interface-definitions/interfaces-geneve.xml.in +++ b/interface-definitions/interfaces-geneve.xml.in @@ -22,7 +22,7 @@ #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> - #include <include/interface/mtu-1450-16000.xml.i> + #include <include/interface/mtu-1200-16000.xml.i> <node name="parameters"> <properties> <help>GENEVE tunnel parameters</help> diff --git a/interface-definitions/interfaces-virtual-ethernet.xml.in b/interface-definitions/interfaces-virtual-ethernet.xml.in index 864f658da..1daa764d4 100644 --- a/interface-definitions/interfaces-virtual-ethernet.xml.in +++ b/interface-definitions/interfaces-virtual-ethernet.xml.in @@ -21,6 +21,8 @@ #include <include/interface/dhcp-options.xml.i> #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable.xml.i> + #include <include/interface/vif-s.xml.i> + #include <include/interface/vif.xml.i> #include <include/interface/vrf.xml.i> <leafNode name="peer-name"> <properties> diff --git a/interface-definitions/interfaces-wireguard.xml.in b/interface-definitions/interfaces-wireguard.xml.in index 6342b21cf..03f169c05 100644 --- a/interface-definitions/interfaces-wireguard.xml.in +++ b/interface-definitions/interfaces-wireguard.xml.in @@ -5,7 +5,7 @@ <tagNode name="wireguard" owner="${vyos_conf_scripts_dir}/interfaces-wireguard.py"> <properties> <help>WireGuard Interface</help> - <priority>459</priority> + <priority>381</priority> <constraint> <regex>wg[0-9]+</regex> </constraint> diff --git a/interface-definitions/service-mdns-repeater.xml.in b/interface-definitions/service-mdns-repeater.xml.in index 4aab9a558..653dbbbe4 100644 --- a/interface-definitions/service-mdns-repeater.xml.in +++ b/interface-definitions/service-mdns-repeater.xml.in @@ -38,6 +38,7 @@ <constraint> <regex>[-_.a-zA-Z0-9]+</regex> </constraint> + <constraintErrorMessage>Service name must be alphanumeric and can contain hyphens and underscores</constraintErrorMessage> <multi/> </properties> </leafNode> diff --git a/interface-definitions/xml-component-version.xml.in b/interface-definitions/xml-component-version.xml.in index e05f64643..8c9e816d1 100644 --- a/interface-definitions/xml-component-version.xml.in +++ b/interface-definitions/xml-component-version.xml.in @@ -10,6 +10,7 @@ #include <include/version/dhcp-relay-version.xml.i> #include <include/version/dhcp-server-version.xml.i> #include <include/version/dhcpv6-server-version.xml.i> + #include <include/version/dns-dynamic-version.xml.i> #include <include/version/dns-forwarding-version.xml.i> #include <include/version/firewall-version.xml.i> #include <include/version/flow-accounting-version.xml.i> diff --git a/op-mode-definitions/dns-dynamic.xml.in b/op-mode-definitions/dns-dynamic.xml.in index 9c37874fb..4f0399964 100644 --- a/op-mode-definitions/dns-dynamic.xml.in +++ b/op-mode-definitions/dns-dynamic.xml.in @@ -1,16 +1,40 @@ <?xml version="1.0"?> <interfaceDefinition> + <node name="monitor"> + <children> + <node name="log"> + <children> + <node name="dns"> + <properties> + <help>Monitor last lines of Domain Name System (DNS) related services</help> + </properties> + <children> + <node name="dynamic"> + <properties> + <help>Monitor last lines of Dynamic DNS update service</help> + </properties> + <command>journalctl --no-hostname --follow --boot --unit ddclient.service</command> + </node> + </children> + </node> + </children> + </node> + </children> + </node> <node name="show"> <children> <node name="log"> <children> <node name="dns"> + <properties> + <help>Show log for Domain Name System (DNS) related services</help> + </properties> <children> <node name="dynamic"> <properties> - <help>Show log for dynamic DNS</help> + <help>Show log for Dynamic DNS update service</help> </properties> - <command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e "ddclient"</command> + <command>journalctl --no-hostname --boot --unit ddclient.service</command> </node> </children> </node> @@ -18,7 +42,7 @@ </node> <node name="dns"> <properties> - <help>Show DNS information</help> + <help>Show Domain Name System (DNS) related information</help> </properties> <children> <node name="dynamic"> @@ -30,7 +54,7 @@ <properties> <help>Show Dynamic DNS status</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --status</command> + <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --status</command> </leafNode> </children> </node> @@ -41,12 +65,15 @@ <node name="restart"> <children> <node name="dns"> + <properties> + <help>Restart specific Domain Name System (DNS) related service</help> + </properties> <children> <node name="dynamic"> <properties> <help>Restart Dynamic DNS service</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command> + <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> </node> </children> </node> @@ -59,14 +86,14 @@ <children> <node name="dns"> <properties> - <help>Update DNS information</help> + <help>Update Domain Name System (DNS) related information</help> </properties> <children> <node name="dynamic"> <properties> <help>Update Dynamic DNS information</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dynamic_dns.py --update</command> + <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> </node> </children> </node> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index c8ca117be..a4c650c38 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -6,7 +6,7 @@ <children> <node name="dns"> <properties> - <help>Monitor last lines of Domain Name Service (DNS)</help> + <help>Monitor last lines of Domain Name System (DNS) related services</help> </properties> <children> <node name="forwarding"> @@ -27,7 +27,7 @@ <children> <node name="dns"> <properties> - <help>Show log for Domain Name Service (DNS)</help> + <help>Show log for Domain Name System (DNS) related services</help> </properties> <children> <node name="forwarding"> @@ -42,7 +42,7 @@ </node> <node name="dns"> <properties> - <help>Show DNS information</help> + <help>Show Domain Name System (DNS) related information</help> </properties> <children> <node name="forwarding"> @@ -66,7 +66,7 @@ <children> <node name="dns"> <properties> - <help>Restart specific DNS service</help> + <help>Restart specific Domain Name System (DNS) related service</help> </properties> <children> <leafNode name="forwarding"> @@ -83,7 +83,7 @@ <children> <node name="dns"> <properties> - <help>Reset a DNS service state</help> + <help>Reset Domain Name System (DNS) related service state</help> </properties> <children> <node name="forwarding"> diff --git a/op-mode-definitions/force-netns.xml.in b/op-mode-definitions/force-netns.xml.in new file mode 100644 index 000000000..b9dc2c1e8 --- /dev/null +++ b/op-mode-definitions/force-netns.xml.in @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <tagNode name="netns"> + <properties> + <help>Execute shell in given Network Namespace</help> + <completionHelp> + <path>netns name</path> + </completionHelp> + </properties> + <command>sudo ip netns exec $3 su - $(whoami)</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/force-vrf.xml.in b/op-mode-definitions/force-vrf.xml.in new file mode 100644 index 000000000..71f50b0d2 --- /dev/null +++ b/op-mode-definitions/force-vrf.xml.in @@ -0,0 +1,16 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <tagNode name="vrf"> + <properties> + <help>Execute shell in given VRF instance</help> + <completionHelp> + <path>vrf name</path> + </completionHelp> + </properties> + <command>sudo ip vrf exec $3 su - $(whoami)</command> + </tagNode> + </children> + </node> +</interfaceDefinition> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index fade3081c..57563a9c1 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -26,6 +26,7 @@ from tabulate import tabulate from vyos.config import Config from vyos.configtree import ConfigTree, ConfigTreeError, show_diff from vyos.defaults import directories +from vyos.version import get_full_version_data from vyos.util import is_systemd_service_active, ask_yes_no, rc_cmd SAVE_CONFIG = '/opt/vyatta/sbin/vyatta-save-config.pl' @@ -56,6 +57,21 @@ formatter = logging.Formatter('%(funcName)s: %(levelname)s:%(message)s') ch.setFormatter(formatter) logger.addHandler(ch) +def save_config(target): + cmd = f'{SAVE_CONFIG} {target}' + rc, out = rc_cmd(cmd) + if rc != 0: + logger.critical(f'save config failed: {out}') + +def unsaved_commits() -> bool: + if get_full_version_data()['boot_via'] == 'livecd': + return False + tmp_save = '/tmp/config.running' + save_config(tmp_save) + ret = not cmp(tmp_save, config_file, shallow=False) + os.unlink(tmp_save) + return ret + class ConfigMgmtError(Exception): pass @@ -98,20 +114,6 @@ class ConfigMgmt: self.active_config = config._running_config self.working_config = config._session_config - @staticmethod - def save_config(target): - cmd = f'{SAVE_CONFIG} {target}' - rc, out = rc_cmd(cmd) - if rc != 0: - logger.critical(f'save config failed: {out}') - - def _unsaved_commits(self) -> bool: - tmp_save = '/tmp/config.boot.check-save' - self.save_config(tmp_save) - ret = not cmp(tmp_save, config_file, shallow=False) - os.unlink(tmp_save) - return ret - # Console script functions # def commit_confirm(self, minutes: int=DEFAULT_TIME_MINUTES, @@ -123,7 +125,7 @@ class ConfigMgmt: msg = 'Another confirm is pending' return msg, 1 - if self._unsaved_commits(): + if unsaved_commits(): W = '\nYou should save previous commits before commit-confirm !\n' else: W = '' @@ -450,7 +452,7 @@ Proceed ?''' ext = os.getpid() tmp_save = f'/tmp/config.boot.{ext}' - self.save_config(tmp_save) + save_config(tmp_save) try: if cmp(tmp_save, archive_config_file, shallow=False): diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index 19b9838d4..d0cd87464 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -201,7 +201,9 @@ class ConfigTree(object): check_path(path) path_str = " ".join(map(str, path)).encode() - self.__delete(self.__config, path_str) + res = self.__delete(self.__config, path_str) + if (res != 0): + raise ConfigTreeError(f"Path doesn't exist: {path}") if self.__migration: print(f"- op: delete path: {path}") @@ -210,7 +212,14 @@ class ConfigTree(object): check_path(path) path_str = " ".join(map(str, path)).encode() - self.__delete_value(self.__config, path_str, value.encode()) + res = self.__delete_value(self.__config, path_str, value.encode()) + if (res != 0): + if res == 1: + raise ConfigTreeError(f"Path doesn't exist: {path}") + elif res == 2: + raise ConfigTreeError(f"Value doesn't exist: '{value}'") + else: + raise ConfigTreeError() if self.__migration: print(f"- op: delete_value path: {path} value: {value}") diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index f62b9f7d2..7754488c4 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -57,6 +57,8 @@ from vyos.ifconfig import Section from netaddr import EUI from netaddr import mac_unix_expanded +link_local_prefix = 'fe80::/64' + class Interface(Control): # This is the class which will be used to create # self.operational, it allows subclasses, such as @@ -1444,7 +1446,7 @@ class Interface(Control): # we will delete all interface specific IP addresses if they are not # explicitly configured on the CLI if is_ipv6_link_local(addr): - eui64 = mac2eui64(self.get_mac(), 'fe80::/64') + eui64 = mac2eui64(self.get_mac(), link_local_prefix) if addr != f'{eui64}/64': self.del_addr(addr) else: @@ -1571,9 +1573,9 @@ class Interface(Control): # Manage IPv6 link-local addresses if dict_search('ipv6.address.no_default_link_local', config) != None: - self.del_ipv6_eui64_address('fe80::/64') + self.del_ipv6_eui64_address(link_local_prefix) else: - self.add_ipv6_eui64_address('fe80::/64') + self.add_ipv6_eui64_address(link_local_prefix) # Add IPv6 EUI-based addresses tmp = dict_search('ipv6.address.eui64', config) diff --git a/python/vyos/util.py b/python/vyos/util.py index d5330db13..61ce59324 100644 --- a/python/vyos/util.py +++ b/python/vyos/util.py @@ -463,14 +463,17 @@ def process_running(pid_file): pid = f.read().strip() return pid_exists(int(pid)) -def process_named_running(name): +def process_named_running(name, cmdline: str=None): """ Checks if process with given name is running and returns its PID. If Process is not running, return None """ from psutil import process_iter - for p in process_iter(): - if name in p.name(): - return p.pid + for p in process_iter(['name', 'pid', 'cmdline']): + if cmdline: + if p.info['name'] == name and cmdline in p.info['cmdline']: + return p.info['pid'] + elif p.info['name'] == name: + return p.info['pid'] return None def is_list_equal(first: list, second: list) -> bool: diff --git a/python/vyos/validate.py b/python/vyos/validate.py index a83193363..d18785aaf 100644 --- a/python/vyos/validate.py +++ b/python/vyos/validate.py @@ -62,7 +62,7 @@ def is_intf_addr_assigned(intf, address) -> bool: # 10: [{'addr': 'fe80::a00:27ff:fed9:5b04%eth0', 'netmask': 'ffff:ffff:ffff:ffff::'}] # } try: - ifaces = ifaddresses(intf) + addresses = ifaddresses(intf) except ValueError as e: print(e) return False @@ -74,7 +74,7 @@ def is_intf_addr_assigned(intf, address) -> bool: netmask = None if '/' in address: address, netmask = address.split('/') - for ip in ifaces.get(addr_type,[]): + for ip in addresses.get(addr_type, []): # ip can have the interface name in the 'addr' field, we need to remove it # {'addr': 'fe80::a00:27ff:fec5:f821%eth2', 'netmask': 'ffff:ffff:ffff:ffff::'} ip_addr = ip['addr'].split('%')[0] diff --git a/python/vyos/xml_ref/__init__.py b/python/vyos/xml_ref/__init__.py index ae5184746..2e144ef10 100644 --- a/python/vyos/xml_ref/__init__.py +++ b/python/vyos/xml_ref/__init__.py @@ -45,6 +45,9 @@ def is_valueless(path: list) -> bool: def is_leaf(path: list) -> bool: return load_reference().is_leaf(path) +def cli_defined(path: list, node: str, non_local=False) -> bool: + return load_reference().cli_defined(path, node, non_local=non_local) + def component_version() -> dict: return load_reference().component_version() diff --git a/python/vyos/xml_ref/definition.py b/python/vyos/xml_ref/definition.py index 429331577..95ecc01a6 100644 --- a/python/vyos/xml_ref/definition.py +++ b/python/vyos/xml_ref/definition.py @@ -92,6 +92,31 @@ class Xml: d = self._get_ref_path(path) return self._is_leaf_node(d) + @staticmethod + def _dict_get(d: dict, path: list) -> dict: + for i in path: + d = d.get(i, {}) + if not isinstance(d, dict): + return {} + if not d: + break + return d + + def _dict_find(self, d: dict, key: str, non_local=False) -> bool: + for k in list(d): + if k in ('node_data', 'component_version'): + continue + if k == key: + return True + if non_local and isinstance(d[k], dict): + if self._dict_find(d[k], key): + return True + return False + + def cli_defined(self, path: list, node: str, non_local=False) -> bool: + d = self._dict_get(self.ref, path) + return self._dict_find(d, node, non_local=non_local) + def component_version(self) -> dict: d = {} for k, v in self.ref['component_version']: diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 2f730abfb..348741715 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -35,6 +35,7 @@ from vyos.util import process_named_running from vyos.util import get_interface_config from vyos.validate import is_intf_addr_assigned from vyos.validate import is_ipv6_link_local +from vyos.xml_ref import cli_defined def is_mirrored_to(interface, mirror_if, qdisc): """ @@ -82,6 +83,17 @@ class BasicInterfaceTest: def setUpClass(cls): super(BasicInterfaceTest.TestCase, cls).setUpClass() + # XXX the case of test_vif_8021q_mtu_limits, below, shows that + # we should extend cli_defined to support more complex queries + cls._test_vlan = cli_defined(cls._base_path, 'vif') + cls._test_qinq = cli_defined(cls._base_path, 'vif-s') + cls._test_dhcp = cli_defined(cls._base_path, 'dhcp-options') + cls._test_ip = cli_defined(cls._base_path, 'ip') + cls._test_ipv6 = cli_defined(cls._base_path, 'ipv6') + cls._test_ipv6_dhcpc6 = cli_defined(cls._base_path, 'dhcpv6-options') + cls._test_ipv6_pd = cli_defined(cls._base_path + ['dhcpv6-options'], 'pd') + cls._test_mtu = cli_defined(cls._base_path, 'mtu') + # Setup mirror interfaces for SPAN (Switch Port Analyzer) for span in cls._mirror_interfaces: section = Section.section(span) @@ -104,9 +116,15 @@ class BasicInterfaceTest: for intf in self._interfaces: self.assertNotIn(intf, interfaces()) - # No daemon that was started during a test should remain running + # No daemon started during tests should remain running for daemon in ['dhcp6c', 'dhclient']: - self.assertFalse(process_named_running(daemon)) + # if _interface list is populated do a more fine grained search + # by also checking the cmd arguments passed to the daemon + if self._interfaces: + for tmp in self._interfaces: + self.assertFalse(process_named_running(daemon, tmp)) + else: + self.assertFalse(process_named_running(daemon)) def test_dhcp_disable_interface(self): if not self._test_dhcp: @@ -360,7 +378,7 @@ class BasicInterfaceTest: # is the Wireless test will first load the wifi kernel hwsim module # which creates a wlan0 and wlan1 interface which will fail the # tearDown() test in the end that no interface is allowed to survive! - if not self._test_vlan: + if not self._test_vlan or not self._test_mtu: self.skipTest('not supported') mtu_1500 = '1500' @@ -634,58 +652,90 @@ class BasicInterfaceTest: self.cli_set(path + option.split()) # Options - self.cli_set(path + ['ip', 'adjust-mss', mss]) - self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo]) - self.cli_set(path + ['ip', 'disable-arp-filter']) - self.cli_set(path + ['ip', 'disable-forwarding']) - self.cli_set(path + ['ip', 'enable-directed-broadcast']) - self.cli_set(path + ['ip', 'enable-arp-accept']) - self.cli_set(path + ['ip', 'enable-arp-announce']) - self.cli_set(path + ['ip', 'enable-arp-ignore']) - self.cli_set(path + ['ip', 'enable-proxy-arp']) - self.cli_set(path + ['ip', 'proxy-arp-pvlan']) - self.cli_set(path + ['ip', 'source-validation', 'loose']) + if cli_defined(self._base_path + ['ip'], 'adjust-mss'): + self.cli_set(path + ['ip', 'adjust-mss', mss]) + + if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'): + self.cli_set(path + ['ip', 'arp-cache-timeout', arp_tmo]) + + if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'): + self.cli_set(path + ['ip', 'disable-arp-filter']) + + if cli_defined(self._base_path + ['ip'], 'disable-forwarding'): + self.cli_set(path + ['ip', 'disable-forwarding']) + + if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'): + self.cli_set(path + ['ip', 'enable-directed-broadcast']) + + if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'): + self.cli_set(path + ['ip', 'enable-arp-accept']) + + if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'): + self.cli_set(path + ['ip', 'enable-arp-announce']) + + if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'): + self.cli_set(path + ['ip', 'enable-arp-ignore']) + + if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'): + self.cli_set(path + ['ip', 'enable-proxy-arp']) + + if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'): + self.cli_set(path + ['ip', 'proxy-arp-pvlan']) + + if cli_defined(self._base_path + ['ip'], 'source-validation'): + self.cli_set(path + ['ip', 'source-validation', 'loose']) self.cli_commit() for interface in self._interfaces: - base_options = f'oifname "{interface}"' - out = cmd('sudo nft list chain raw VYOS_TCP_MSS') - for line in out.splitlines(): - if line.startswith(base_options): - self.assertIn(f'tcp option maxseg size set {mss}', line) + if cli_defined(self._base_path + ['ip'], 'adjust-mss'): + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain raw VYOS_TCP_MSS') + for line in out.splitlines(): + if line.startswith(base_options): + self.assertIn(f'tcp option maxseg size set {mss}', line) - tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') - self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds + if cli_defined(self._base_path + ['ip'], 'arp-cache-timeout'): + tmp = read_file(f'/proc/sys/net/ipv4/neigh/{interface}/base_reachable_time_ms') + self.assertEqual(tmp, str((int(arp_tmo) * 1000))) # tmo value is in milli seconds proc_base = f'/proc/sys/net/ipv4/conf/{interface}' - tmp = read_file(f'{proc_base}/arp_filter') - self.assertEqual('0', tmp) + if cli_defined(self._base_path + ['ip'], 'disable-arp-filter'): + tmp = read_file(f'{proc_base}/arp_filter') + self.assertEqual('0', tmp) - tmp = read_file(f'{proc_base}/arp_accept') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'enable-arp-accept'): + tmp = read_file(f'{proc_base}/arp_accept') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/arp_announce') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'enable-arp-announce'): + tmp = read_file(f'{proc_base}/arp_announce') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/arp_ignore') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'enable-arp-ignore'): + tmp = read_file(f'{proc_base}/arp_ignore') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/forwarding') - self.assertEqual('0', tmp) + if cli_defined(self._base_path + ['ip'], 'disable-forwarding'): + tmp = read_file(f'{proc_base}/forwarding') + self.assertEqual('0', tmp) - tmp = read_file(f'{proc_base}/bc_forwarding') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'enable-directed-broadcast'): + tmp = read_file(f'{proc_base}/bc_forwarding') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/proxy_arp') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'enable-proxy-arp'): + tmp = read_file(f'{proc_base}/proxy_arp') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/proxy_arp_pvlan') - self.assertEqual('1', tmp) + if cli_defined(self._base_path + ['ip'], 'proxy-arp-pvlan'): + tmp = read_file(f'{proc_base}/proxy_arp_pvlan') + self.assertEqual('1', tmp) - tmp = read_file(f'{proc_base}/rp_filter') - self.assertEqual('2', tmp) + if cli_defined(self._base_path + ['ip'], 'source-validation'): + tmp = read_file(f'{proc_base}/rp_filter') + self.assertEqual('2', tmp) def test_interface_ipv6_options(self): if not self._test_ipv6: @@ -700,26 +750,33 @@ class BasicInterfaceTest: self.cli_set(path + option.split()) # Options - self.cli_set(path + ['ipv6', 'adjust-mss', mss]) - self.cli_set(path + ['ipv6', 'disable-forwarding']) - self.cli_set(path + ['ipv6', 'dup-addr-detect-transmits', dad_transmits]) + if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'): + self.cli_set(path + ['ipv6', 'adjust-mss', mss]) + + if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'): + self.cli_set(path + ['ipv6', 'dup-addr-detect-transmits', dad_transmits]) + + if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'): + self.cli_set(path + ['ipv6', 'disable-forwarding']) self.cli_commit() for interface in self._interfaces: - base_options = f'oifname "{interface}"' - out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS') - for line in out.splitlines(): - if line.startswith(base_options): - self.assertIn(f'tcp option maxseg size set {mss}', line) - proc_base = f'/proc/sys/net/ipv6/conf/{interface}' + if cli_defined(self._base_path + ['ipv6'], 'adjust-mss'): + base_options = f'oifname "{interface}"' + out = cmd('sudo nft list chain ip6 raw VYOS_TCP_MSS') + for line in out.splitlines(): + if line.startswith(base_options): + self.assertIn(f'tcp option maxseg size set {mss}', line) - tmp = read_file(f'{proc_base}/forwarding') - self.assertEqual('0', tmp) + if cli_defined(self._base_path + ['ipv6'], 'dup-addr-detect-transmits'): + tmp = read_file(f'{proc_base}/dad_transmits') + self.assertEqual(dad_transmits, tmp) - tmp = read_file(f'{proc_base}/dad_transmits') - self.assertEqual(dad_transmits, tmp) + if cli_defined(self._base_path + ['ipv6'], 'disable-forwarding'): + tmp = read_file(f'{proc_base}/forwarding') + self.assertEqual('0', tmp) def test_dhcpv6_client_options(self): if not self._test_ipv6_dhcpc6: @@ -765,7 +822,14 @@ class BasicInterfaceTest: prefix_len = '56' sla_len = str(64 - int(prefix_len)) + # Create delegatee interfaces first to avoid any confusion by dhcpc6 + # this is mainly an "issue" with virtual-ethernet interfaces delegatees = ['dum2340', 'dum2341', 'dum2342', 'dum2343', 'dum2344'] + for delegatee in delegatees: + section = Section.section(delegatee) + self.cli_set(['interfaces', section, delegatee]) + + self.cli_commit() for interface in self._interfaces: path = self._base_path + [interface] @@ -778,8 +842,6 @@ class BasicInterfaceTest: self.cli_set(pd_base + ['length', prefix_len]) for delegatee in delegatees: - section = Section.section(delegatee) - self.cli_set(['interfaces', section, delegatee]) self.cli_set(pd_base + ['interface', delegatee, 'address', address]) # increment interface address address = str(int(address) + 1) @@ -821,7 +883,14 @@ class BasicInterfaceTest: prefix_len = '56' sla_len = str(64 - int(prefix_len)) + # Create delegatee interfaces first to avoid any confusion by dhcpc6 + # this is mainly an "issue" with virtual-ethernet interfaces delegatees = ['dum3340', 'dum3341', 'dum3342', 'dum3343', 'dum3344'] + for delegatee in delegatees: + section = Section.section(delegatee) + self.cli_set(['interfaces', section, delegatee]) + + self.cli_commit() for interface in self._interfaces: path = self._base_path + [interface] @@ -835,8 +904,6 @@ class BasicInterfaceTest: self.cli_set(pd_base + ['length', prefix_len]) for delegatee in delegatees: - section = Section.section(delegatee) - self.cli_set(['interfaces', section, delegatee]) self.cli_set(pd_base + ['interface', delegatee, 'address', address]) self.cli_set(pd_base + ['interface', delegatee, 'sla-id', sla_id]) @@ -866,8 +933,8 @@ class BasicInterfaceTest: # increment interface address address = str(int(address) + 1) - # Check for running process - self.assertTrue(process_named_running('dhcp6c')) + # Check for running process + self.assertTrue(process_named_running('dhcp6c', interface)) for delegatee in delegatees: # we can already cleanup the test delegatee interface here diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index cd3995ed9..2e09173a7 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -28,14 +28,6 @@ from vyos.util import read_file class BondingInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True - cls._test_ip = True - cls._test_ipv6 = True - cls._test_ipv6_pd = True - cls._test_ipv6_dhcpc6 = True - cls._test_mtu = True - cls._test_vlan = True - cls._test_qinq = True cls._base_path = ['interfaces', 'bonding'] cls._mirror_interfaces = ['dum21354'] cls._members = [] @@ -251,4 +243,4 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): self.assertIn(member, slaves) if __name__ == '__main__': - unittest.main(verbosity=2) + unittest.main(verbosity=2, failfast=True) diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 6d7af78eb..85b45c8fa 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -32,12 +32,6 @@ from vyos.validate import is_intf_addr_assigned class BridgeInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True - cls._test_ip = True - cls._test_ipv6 = True - cls._test_ipv6_pd = True - cls._test_ipv6_dhcpc6 = True - cls._test_vlan = True cls._base_path = ['interfaces', 'bridge'] cls._mirror_interfaces = ['dum21354'] cls._members = [] diff --git a/smoketest/scripts/cli/test_interfaces_dummy.py b/smoketest/scripts/cli/test_interfaces_dummy.py index a79e4cb1b..d96ec2c5d 100755 --- a/smoketest/scripts/cli/test_interfaces_dummy.py +++ b/smoketest/scripts/cli/test_interfaces_dummy.py @@ -21,7 +21,6 @@ from base_interfaces_test import BasicInterfaceTest class DummyInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_mtu = True cls._base_path = ['interfaces', 'dummy'] cls._interfaces = ['dum435', 'dum8677', 'dum0931', 'dum089'] # call base-classes classmethod diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index e53413f0d..a1bc8481d 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -109,14 +109,6 @@ def get_certificate_count(interface, cert_type): class EthernetInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True - cls._test_ip = True - cls._test_ipv6 = True - cls._test_ipv6_pd = True - cls._test_ipv6_dhcpc6 = True - cls._test_mtu = True - cls._test_vlan = True - cls._test_qinq = True cls._base_path = ['interfaces', 'ethernet'] cls._mirror_interfaces = ['dum21354'] diff --git a/smoketest/scripts/cli/test_interfaces_geneve.py b/smoketest/scripts/cli/test_interfaces_geneve.py index 0e5098aa7..24d350aeb 100755 --- a/smoketest/scripts/cli/test_interfaces_geneve.py +++ b/smoketest/scripts/cli/test_interfaces_geneve.py @@ -24,8 +24,6 @@ from base_interfaces_test import BasicInterfaceTest class GeneveInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True - cls._test_ipv6 = True cls._base_path = ['interfaces', 'geneve'] cls._options = { 'gnv0': ['vni 10', 'remote 127.0.1.1'], diff --git a/smoketest/scripts/cli/test_interfaces_input.py b/smoketest/scripts/cli/test_interfaces_input.py index c6d7febec..b4cae4695 100755 --- a/smoketest/scripts/cli/test_interfaces_input.py +++ b/smoketest/scripts/cli/test_interfaces_input.py @@ -27,7 +27,6 @@ class InputInterfaceTest(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(InputInterfaceTest, cls).setUpClass() - cls._interfaces = ['ifb10', 'ifb20', 'ifb30'] def tearDown(self): diff --git a/smoketest/scripts/cli/test_interfaces_l2tpv3.py b/smoketest/scripts/cli/test_interfaces_l2tpv3.py index aed8e6f15..b1d5b7c19 100755 --- a/smoketest/scripts/cli/test_interfaces_l2tpv3.py +++ b/smoketest/scripts/cli/test_interfaces_l2tpv3.py @@ -24,8 +24,6 @@ from vyos.util import cmd class L2TPv3InterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True - cls._test_ipv6 = True cls._base_path = ['interfaces', 'l2tpv3'] cls._options = { 'l2tpeth10': ['source-address 127.0.0.1', 'remote 127.10.10.10', diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py index 5ff9c250e..cde90189b 100755 --- a/smoketest/scripts/cli/test_interfaces_loopback.py +++ b/smoketest/scripts/cli/test_interfaces_loopback.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -31,6 +31,9 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(LoopbackInterfaceTest, cls).setUpClass() + # we need to override tearDown() as loopback interfaces are ephemeral and + # will always be present on the system - the base class check will fail as + # the loopback interface will still exist. def tearDown(self): self.cli_delete(self._base_path) self.cli_commit() diff --git a/smoketest/scripts/cli/test_interfaces_macsec.py b/smoketest/scripts/cli/test_interfaces_macsec.py index f141cc6d3..433336f07 100755 --- a/smoketest/scripts/cli/test_interfaces_macsec.py +++ b/smoketest/scripts/cli/test_interfaces_macsec.py @@ -42,9 +42,6 @@ def get_cipher(interface): class MACsecInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True - cls._test_ip = True - cls._test_ipv6 = True cls._base_path = ['interfaces', 'macsec'] cls._options = { 'macsec0': ['source-interface eth0', 'security cipher gcm-aes-128'] } diff --git a/smoketest/scripts/cli/test_interfaces_netns.py b/smoketest/scripts/cli/test_interfaces_netns.py index 9975a6b09..dffa7e0ac 100755 --- a/smoketest/scripts/cli/test_interfaces_netns.py +++ b/smoketest/scripts/cli/test_interfaces_netns.py @@ -14,9 +14,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 re -import os -import json import unittest from netifaces import interfaces @@ -32,7 +29,6 @@ base_path = ['netns'] namespaces = ['mgmt', 'front', 'back', 'ams-ix'] class NETNSTest(VyOSUnitTestSHIM.TestCase): - def setUp(self): self._interfaces = ['dum10', 'dum12', 'dum50'] diff --git a/smoketest/scripts/cli/test_interfaces_pppoe.py b/smoketest/scripts/cli/test_interfaces_pppoe.py index f4efed641..0ce5e2fe0 100755 --- a/smoketest/scripts/cli/test_interfaces_pppoe.py +++ b/smoketest/scripts/cli/test_interfaces_pppoe.py @@ -14,7 +14,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 re import unittest from psutil import process_iter diff --git a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py index a51b8d52c..0d6f5bc1f 100755 --- a/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_pseudo_ethernet.py @@ -23,14 +23,6 @@ from base_interfaces_test import BasicInterfaceTest class PEthInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True - cls._test_ip = True - cls._test_ipv6 = True - cls._test_ipv6_pd = True - cls._test_ipv6_dhcpc6 = True - cls._test_mtu = True - cls._test_vlan = True - cls._test_qinq = True cls._base_path = ['interfaces', 'pseudo-ethernet'] cls._options = {} diff --git a/smoketest/scripts/cli/test_interfaces_tunnel.py b/smoketest/scripts/cli/test_interfaces_tunnel.py index 44bfbb5f0..9dce2b52c 100755 --- a/smoketest/scripts/cli/test_interfaces_tunnel.py +++ b/smoketest/scripts/cli/test_interfaces_tunnel.py @@ -30,9 +30,6 @@ mtu = 1476 class TunnelInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True - cls._test_ipv6 = True - cls._test_mtu = True cls._base_path = ['interfaces', 'tunnel'] cls.local_v4 = '192.0.2.1' cls.local_v6 = '2001:db8::1' diff --git a/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py index 4732342fc..4710930a0 100755 --- a/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_virtual_ethernet.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,18 +14,18 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os import unittest +from netifaces import interfaces + from vyos.ifconfig import Section +from vyos.util import process_named_running from base_interfaces_test import BasicInterfaceTest class VEthInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_dhcp = True cls._base_path = ['interfaces', 'virtual-ethernet'] - cls._options = { 'veth0': ['peer-name veth1'], 'veth1': ['peer-name veth0'], @@ -35,5 +35,28 @@ class VEthInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(VEthInterfaceTest, cls).setUpClass() + def test_vif_8021q_mtu_limits(self): + self.skipTest('not supported') + + # As we always need a pair of veth interfaces, we can not rely on the base + # class check to determine if there is a dhcp6c or dhclient instance running. + # This test will always fail as there is an instance running on the peer + # interface. + def tearDown(self): + self.cli_delete(self._base_path) + self.cli_commit() + + # Verify that no previously interface remained on the system + for intf in self._interfaces: + self.assertNotIn(intf, interfaces()) + + @classmethod + def tearDownClass(cls): + # No daemon started during tests should remain running + for daemon in ['dhcp6c', 'dhclient']: + cls.assertFalse(cls, process_named_running(daemon)) + + super(VEthInterfaceTest, cls).tearDownClass() + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py index 9cbf104f0..7f13575a3 100755 --- a/smoketest/scripts/cli/test_interfaces_vti.py +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2023 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 @@ -21,9 +21,6 @@ from base_interfaces_test import BasicInterfaceTest class VTIInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True - cls._test_ipv6 = True - cls._test_mtu = True cls._base_path = ['interfaces', 'vti'] cls._interfaces = ['vti10', 'vti20', 'vti30'] diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py index 058f13721..60a9d1966 100755 --- a/smoketest/scripts/cli/test_interfaces_vxlan.py +++ b/smoketest/scripts/cli/test_interfaces_vxlan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -26,9 +26,6 @@ from base_interfaces_test import BasicInterfaceTest class VXLANInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True - cls._test_ipv6 = True - cls._test_mtu = True cls._base_path = ['interfaces', 'vxlan'] cls._options = { 'vxlan10': ['vni 10', 'remote 127.0.0.2'], diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py index a24f37d8d..85432cf04 100755 --- a/smoketest/scripts/cli/test_interfaces_wireless.py +++ b/smoketest/scripts/cli/test_interfaces_wireless.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -34,7 +34,6 @@ def get_config_value(interface, key): class WirelessInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): - cls._test_ip = True cls._base_path = ['interfaces', 'wireless'] cls._options = { 'wlan0': ['physical-device phy0', 'ssid VyOS-WIFI-0', @@ -50,6 +49,10 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(WirelessInterfaceTest, cls).setUpClass() + # T5245 - currently testcases are disabled + cls._test_ipv6 = False + cls._test_vlan = False + def test_wireless_add_single_ip_address(self): # derived method to check if member interfaces are enslaved properly super().test_add_single_ip_address() diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index a3aa41f94..11d411cb4 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2020 VyOS maintainers and contributors +# Copyright (C) 2019-2023 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 @@ -17,155 +17,167 @@ import re import os import unittest +import tempfile from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError from vyos.util import cmd -from vyos.util import process_named_running -from vyos.util import read_file +from vyos.util import process_running -PROCESS_NAME = 'ddclient' DDCLIENT_CONF = '/run/ddclient/ddclient.conf' +DDCLIENT_PID = '/run/ddclient/ddclient.pid' base_path = ['service', 'dns', 'dynamic'] hostname = 'test.ddns.vyos.io' +zone = 'vyos.io' +password = 'paSS_@4ord' interface = 'eth0' + def get_config_value(key): tmp = cmd(f'sudo cat {DDCLIENT_CONF}') - tmp = re.findall(r'\n?{}=+(.*)'.format(key), tmp) - tmp = tmp[0].rstrip(',') - return tmp + vals = re.findall(r'\n?{}=([.-@_A-Za-z0-9]+),? \\'.format(key), tmp) + return vals[0] if vals else '' + class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def tearDown(self): + # Check for running process + self.assertTrue(process_running(DDCLIENT_PID)) + # Delete DDNS configuration self.cli_delete(base_path) self.cli_commit() - def test_dyndns_service(self): - from itertools import product - ddns = ['interface', interface, 'service'] - users = [None, 'vyos_user'] - services = ['cloudflare', 'afraid', 'dyndns', 'zoneedit'] + # PID file must no londer exist after process exited + self.assertFalse(os.path.exists(DDCLIENT_PID)) + + # IPv4 standard DDNS service configuration + def test_dyndns_service_standard(self): + ddns = ['address', interface, 'service'] + services = {'cloudflare': {'protocol': 'cloudflare'}, + 'freedns': {'protocol': 'freedns', 'username': 'vyos_user'}, + 'zoneedit': {'protocol': 'zoneedit1', 'username': 'vyos_user'}} - for user, service in product(users, services): - password = 'vyos_pass' - zone = 'vyos.io' + for svc, details in services.items(): self.cli_delete(base_path) - self.cli_set(base_path + ddns + [service, 'host-name', hostname]) - if user is not None: - self.cli_set(base_path + ddns + [service, 'login', user]) - self.cli_set(base_path + ddns + [service, 'password', password]) - self.cli_set(base_path + ddns + [service, 'zone', zone]) + self.cli_set(base_path + ddns + [svc, 'host-name', hostname]) + for opt, value in details.items(): + self.cli_set(base_path + ddns + [svc, opt, value]) + self.cli_set(base_path + ddns + [svc, 'password', password]) + self.cli_set(base_path + ddns + [svc, 'zone', zone]) # commit changes - if service == 'cloudflare': + if details['protocol'] == 'cloudflare': self.cli_commit() - elif user is None: - # not set user is only allowed for cloudflare - with self.assertRaises(ConfigSessionError): - # remove zone to test not set user - self.cli_delete(base_path + ddns + [service, 'zone', 'vyos.io']) - self.cli_commit() - # this case is fininshed, user not set is not allowed when service isn't cloudflare - continue else: - # zone option only works on cloudflare, an exception is raised - # for all others + # zone option does not work on all protocols, an exception is + # raised for all others with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + ddns + [service, 'zone', 'vyos.io']) + self.cli_delete(base_path + ddns + [svc, 'zone', zone]) # commit changes again - now it should work self.cli_commit() - # we can only read the configuration file when we operate as 'root' - protocol = get_config_value('protocol') - login = None if user is None else get_config_value('login') - pwd = get_config_value('password') - - # some services need special treatment - protoname = service - if service == 'cloudflare': - tmp = get_config_value('zone') - self.assertTrue(tmp == zone) - elif service == 'afraid': - protoname = 'freedns' - elif service == 'dyndns': - protoname = 'dyndns2' - elif service == 'zoneedit': - protoname = 'zoneedit1' - - self.assertTrue(protocol == protoname) - self.assertTrue(login == user) - self.assertTrue(pwd == "'" + password + "'") - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - def test_dyndns_rfc2136(self): - # Check if DDNS service can be configured and runs - ddns = ['interface', interface, 'rfc2136', 'vyos'] - ddns_key_file = '/config/auth/my.key' - - self.cli_set(base_path + ddns + ['key', ddns_key_file]) - self.cli_set(base_path + ddns + ['record', 'test.ddns.vyos.io']) - self.cli_set(base_path + ddns + ['server', 'ns1.vyos.io']) - self.cli_set(base_path + ddns + ['ttl', '300']) - self.cli_set(base_path + ddns + ['zone', 'vyos.io']) + # Check the generating config parameters + self.assertEqual(get_config_value('use'), 'if') + self.assertEqual(get_config_value('if'), interface) + self.assertEqual(get_config_value('password'), password) - # ensure an exception will be raised as no key is present - if os.path.exists(ddns_key_file): - os.unlink(ddns_key_file) + for opt in details.keys(): + if opt == 'username': + self.assertEqual(get_config_value('login'), details[opt]) + else: + self.assertEqual(get_config_value(opt), details[opt]) - # check validate() - the key file does not exist yet - with self.assertRaises(ConfigSessionError): - self.cli_commit() - - with open(ddns_key_file, 'w') as f: - f.write('S3cretKey') - - # commit changes - self.cli_commit() - - # TODO: inspect generated configuration file - - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - def test_dyndns_ipv6(self): - ddns = ['interface', interface, 'service', 'dynv6'] + # IPv6 only DDNS service configuration + def test_dyndns_service_ipv6(self): + ddns = ['address', interface, 'service', 'dynv6'] proto = 'dyndns2' user = 'none' password = 'paSS_4ord' srv = 'ddns.vyos.io' + ip_version = 'ipv6' - self.cli_set(base_path + ['interface', interface, 'ipv6-enable']) - self.cli_set(base_path + ddns + ['host-name', hostname]) - self.cli_set(base_path + ddns + ['login', user]) - self.cli_set(base_path + ddns + ['password', password]) + self.cli_set(base_path + ddns + ['ip-version', ip_version]) self.cli_set(base_path + ddns + ['protocol', proto]) self.cli_set(base_path + ddns + ['server', srv]) + self.cli_set(base_path + ddns + ['username', user]) + self.cli_set(base_path + ddns + ['password', password]) + self.cli_set(base_path + ddns + ['host-name', hostname]) # commit changes self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - - protocol = get_config_value('protocol') - login = get_config_value('login') - pwd = get_config_value('password') - server = get_config_value('server') - usev6 = get_config_value('usev6') - - # Check some generating config parameters - self.assertEqual(protocol, proto) - self.assertEqual(login, user) - self.assertEqual(pwd, f"'{password}'") - self.assertEqual(server, srv) - self.assertEqual(usev6, f"ifv6, if={interface}") + # Check the generating config parameters + self.assertEqual(get_config_value('usev6'), 'ifv6') + self.assertEqual(get_config_value('ifv6'), interface) + self.assertEqual(get_config_value('protocol'), proto) + self.assertEqual(get_config_value('server'), srv) + self.assertEqual(get_config_value('login'), user) + self.assertEqual(get_config_value('password'), password) + + # IPv4+IPv6 dual DDNS service configuration + def test_dyndns_service_dual_stack(self): + ddns = ['address', interface, 'service'] + services = {'cloudflare': {'protocol': 'cloudflare', 'zone': 'vyos.io'}, + 'freedns': {'protocol': 'freedns', 'username': 'vyos_user'}} + password = 'vyos_pass' + ip_version = 'both' + + for svc, details in services.items(): + self.cli_delete(base_path) + self.cli_set(base_path + ddns + [svc, 'host-name', hostname]) + for opt, value in details.items(): + self.cli_set(base_path + ddns + [svc, opt, value]) + self.cli_set(base_path + ddns + [svc, 'password', password]) + self.cli_set(base_path + ddns + [svc, 'ip-version', ip_version]) + + # commit changes + self.cli_commit() + + # Check the generating config parameters + self.assertEqual(get_config_value('usev4'), 'ifv4') + self.assertEqual(get_config_value('usev6'), 'ifv6') + self.assertEqual(get_config_value('ifv4'), interface) + self.assertEqual(get_config_value('ifv6'), interface) + self.assertEqual(get_config_value('password'), password) + + for opt in details.keys(): + if opt == 'username': + self.assertEqual(get_config_value('login'), details[opt]) + else: + self.assertEqual(get_config_value(opt), details[opt]) + + def test_dyndns_rfc2136(self): + # Check if DDNS service can be configured and runs + ddns = ['address', interface, 'rfc2136', 'vyos'] + srv = 'ns1.vyos.io' + zone = 'vyos.io' + ttl = '300' + + with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: + key_file.write(b'S3cretKey') + + self.cli_set(base_path + ddns + ['server', srv]) + self.cli_set(base_path + ddns + ['zone', zone]) + self.cli_set(base_path + ddns + ['key', key_file.name]) + self.cli_set(base_path + ddns + ['ttl', ttl]) + self.cli_set(base_path + ddns + ['host-name', hostname]) + + # commit changes + self.cli_commit() + + # Check some generating config parameters + self.assertEqual(get_config_value('use'), 'if') + self.assertEqual(get_config_value('if'), interface) + self.assertEqual(get_config_value('protocol'), 'nsupdate') + self.assertEqual(get_config_value('server'), srv) + self.assertEqual(get_config_value('zone'), zone) + self.assertEqual(get_config_value('password'), key_file.name) + self.assertEqual(get_config_value('ttl'), ttl) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_mdns-repeater.py b/smoketest/scripts/cli/test_service_mdns-repeater.py index f99a98da1..880c13043 100755 --- a/smoketest/scripts/cli/test_service_mdns-repeater.py +++ b/smoketest/scripts/cli/test_service_mdns-repeater.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020 VyOS maintainers and contributors +# Copyright (C) 2020-2023 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 @@ -18,30 +18,57 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from configparser import ConfigParser from vyos.util import process_named_running base_path = ['service', 'mdns', 'repeater'] intf_base = ['interfaces', 'dummy'] +config_file = '/run/avahi-daemon/avahi-daemon.conf' + class TestServiceMDNSrepeater(VyOSUnitTestSHIM.TestCase): def tearDown(self): + # Check for running process + self.assertTrue(process_named_running('avahi-daemon')) + self.cli_delete(base_path) self.cli_delete(intf_base + ['dum10']) self.cli_delete(intf_base + ['dum20']) self.cli_commit() + # Check that there is no longer a running process + self.assertFalse(process_named_running('avahi-daemon')) + def test_service(self): - # Service required a configured IP address on the interface + # mDNS browsing domains in addition to the default one (local) + domains = ['dom1.home.arpa', 'dom2.home.arpa'] + + # mDNS services to be repeated + services = ['_ipp._tcp', '_smb._tcp', '_ssh._tcp'] + # Service required a configured IP address on the interface self.cli_set(intf_base + ['dum10', 'address', '192.0.2.1/30']) self.cli_set(intf_base + ['dum20', 'address', '192.0.2.5/30']) self.cli_set(base_path + ['interface', 'dum10']) self.cli_set(base_path + ['interface', 'dum20']) + + for domain in domains: + self.cli_set(base_path + ['browse-domain', domain]) + + for service in services: + self.cli_set(base_path + ['allow-service', service]) + self.cli_commit() - # Check for running process - self.assertTrue(process_named_running('avahi-daemon')) + # Validate configuration values + conf = ConfigParser(delimiters='=') + conf.read(config_file) + + self.assertEqual(conf['server']['allow-interfaces'], 'dum10, dum20') + self.assertEqual(conf['server']['browse-domains'], ', '.join(domains)) + self.assertEqual(conf['reflector']['enable-reflector'], 'yes') + self.assertEqual(conf['reflector']['reflect-filters'], ', '.join(services)) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index b677f0e45..acb41e410 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -41,7 +41,7 @@ vif = '100' esp_group = 'MyESPGroup' ike_group = 'MyIKEGroup' secret = 'MYSECRETKEY' -PROCESS_NAME = 'charon' +PROCESS_NAME = 'charon-systemd' regex_uuid4 = '[0-9a-fA-F]{8}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{4}\-[0-9a-fA-F]{12}' ca_pem = """ diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py new file mode 100755 index 000000000..67134e681 --- /dev/null +++ b/src/conf_mode/dns_dynamic.py @@ -0,0 +1,142 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2018-2023 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 os + +from sys import exit + +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.template import render +from vyos.util import call +from vyos.xml import defaults +from vyos import ConfigError +from vyos import airbag +airbag.enable() + +config_file = r'/run/ddclient/ddclient.conf' +systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' + +# Protocols that require zone +zone_allowed = ['cloudflare', 'godaddy', 'hetzner', 'gandi', 'nfsn'] + +# Protocols that do not require username +username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla'] + +# Protocols that support both IPv4 and IPv6 +dualstack_supported = ['cloudflare', 'dyndns2', 'freedns', 'njalla'] + +def get_config(config=None): + if config: + conf = config + else: + conf = Config() + + base_level = ['service', 'dns', 'dynamic'] + if not conf.exists(base_level): + return None + + dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True) + + if 'address' in dyndns: + for address in dyndns['address']: + # Apply service specific defaults (svc_type = ['rfc2136', 'service']) + for svc_type in dyndns['address'][address]: + default_values = defaults(base_level + ['address', svc_type]) + for svc_cfg in dyndns['address'][address][svc_type]: + dyndns['address'][address][svc_type][svc_cfg] = dict_merge( + default_values, dyndns['address'][address][svc_type][svc_cfg]) + + dyndns['config_file'] = config_file + return dyndns + +def verify(dyndns): + # bail out early - looks like removal from running config + if not dyndns or 'address' not in dyndns: + return None + + for address in dyndns['address']: + # RFC2136 - configuration validation + if 'rfc2136' in dyndns['address'][address]: + for config in dyndns['address'][address]['rfc2136'].values(): + for field in ['host_name', 'zone', 'server', 'key']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" is required for RFC2136 ' + f'based Dynamic DNS service on "{address}"') + + # Dynamic DNS service provider - configuration validation + if 'service' in dyndns['address'][address]: + for service, config in dyndns['address'][address]['service'].items(): + error_msg = f'is required for Dynamic DNS service "{service}" on "{address}"' + + for field in ['host_name', 'password', 'protocol']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg}') + + if config['protocol'] in zone_allowed and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg}') + + if config['protocol'] not in zone_allowed and 'zone' in config: + raise ConfigError(f'"{config["protocol"]}" does not support "zone"') + + if config['protocol'] not in username_unnecessary: + if 'username' not in config: + raise ConfigError(f'"username" {error_msg}') + + if config['ip_version'] == 'both': + if config['protocol'] not in dualstack_supported: + raise ConfigError(f'"{config["protocol"]}" does not support ' + f'both IPv4 and IPv6 at the same time') + # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) + if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] != 'members.dyndns.org': + raise ConfigError(f'"{config["protocol"]}" does not support ' + f'both IPv4 and IPv6 at the same time for "{config["server"]}"') + + return None + +def generate(dyndns): + # bail out early - looks like removal from running config + if not dyndns or 'address' not in dyndns: + return None + + render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns) + render(systemd_override, 'dns-dynamic/override.conf.j2', dyndns) + return None + +def apply(dyndns): + systemd_service = 'ddclient.service' + # Reload systemd manager configuration + call('systemctl daemon-reload') + + # bail out early - looks like removal from running config + if not dyndns or 'address' not in dyndns: + call(f'systemctl stop {systemd_service}') + if os.path.exists(config_file): + os.unlink(config_file) + else: + call(f'systemctl reload-or-restart {systemd_service}') + + return None + +if __name__ == '__main__': + try: + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/dynamic_dns.py b/src/conf_mode/dynamic_dns.py deleted file mode 100755 index 426e3d693..000000000 --- a/src/conf_mode/dynamic_dns.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os - -from sys import exit - -from vyos.config import Config -from vyos.configdict import dict_merge -from vyos.template import render -from vyos.util import call -from vyos.xml import defaults -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -config_file = r'/run/ddclient/ddclient.conf' - -# Mapping of service name to service protocol -default_service_protocol = { - 'afraid': 'freedns', - 'changeip': 'changeip', - 'cloudflare': 'cloudflare', - 'dnspark': 'dnspark', - 'dslreports': 'dslreports1', - 'dyndns': 'dyndns2', - 'easydns': 'easydns', - 'namecheap': 'namecheap', - 'noip': 'noip', - 'sitelutions': 'sitelutions', - 'zoneedit': 'zoneedit1' -} - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - base_level = ['service', 'dns', 'dynamic'] - if not conf.exists(base_level): - return None - - dyndns = conf.get_config_dict(base_level, key_mangling=('-', '_'), get_first_key=True) - - # We have gathered the dict representation of the CLI, but there are default - # options which we need to update into the dictionary retrived. - for interface in dyndns['interface']: - if 'service' in dyndns['interface'][interface]: - # 'Autodetect' protocol used by DynDNS service - for service in dyndns['interface'][interface]['service']: - if service in default_service_protocol: - dyndns['interface'][interface]['service'][service].update( - {'protocol' : default_service_protocol.get(service)}) - else: - dyndns['interface'][interface]['service'][service].update( - {'custom': ''}) - - if 'rfc2136' in dyndns['interface'][interface]: - default_values = defaults(base_level + ['interface', 'rfc2136']) - for rfc2136 in dyndns['interface'][interface]['rfc2136']: - dyndns['interface'][interface]['rfc2136'][rfc2136] = dict_merge( - default_values, dyndns['interface'][interface]['rfc2136'][rfc2136]) - - return dyndns - -def verify(dyndns): - # bail out early - looks like removal from running config - if not dyndns: - return None - - # A 'node' corresponds to an interface - if 'interface' not in dyndns: - return None - - for interface in dyndns['interface']: - # RFC2136 - configuration validation - if 'rfc2136' in dyndns['interface'][interface]: - for rfc2136, config in dyndns['interface'][interface]['rfc2136'].items(): - - for tmp in ['record', 'zone', 'server', 'key']: - if tmp not in config: - raise ConfigError(f'"{tmp}" required for rfc2136 based ' - f'DynDNS service on "{interface}"') - - if not os.path.isfile(config['key']): - raise ConfigError(f'"key"-file not found for rfc2136 based ' - f'DynDNS service on "{interface}"') - - # DynDNS service provider - configuration validation - if 'service' in dyndns['interface'][interface]: - for service, config in dyndns['interface'][interface]['service'].items(): - error_msg = f'required for DynDNS service "{service}" on "{interface}"' - if 'host_name' not in config: - raise ConfigError(f'"host-name" {error_msg}') - - if 'login' not in config: - if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'): - raise ConfigError(f'"login" (username) {error_msg}, unless using CloudFlare') - - if 'password' not in config: - raise ConfigError(f'"password" {error_msg}') - - if 'zone' in config: - if service != 'cloudflare' and ('protocol' not in config or config['protocol'] != 'cloudflare'): - raise ConfigError(f'"zone" option only supported with CloudFlare') - - if 'custom' in config: - if 'protocol' not in config: - raise ConfigError(f'"protocol" {error_msg}') - - if 'server' not in config: - raise ConfigError(f'"server" {error_msg}') - - return None - -def generate(dyndns): - # bail out early - looks like removal from running config - if not dyndns: - return None - - render(config_file, 'dynamic-dns/ddclient.conf.j2', dyndns) - return None - -def apply(dyndns): - if not dyndns: - call('systemctl stop ddclient.service') - if os.path.exists(config_file): - os.unlink(config_file) - else: - call('systemctl restart ddclient.service') - - return None - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - exit(1) diff --git a/src/conf_mode/flow_accounting_conf.py b/src/conf_mode/flow_accounting_conf.py index f67f1710e..c36d52e05 100755 --- a/src/conf_mode/flow_accounting_conf.py +++ b/src/conf_mode/flow_accounting_conf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2023 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 @@ -18,15 +18,13 @@ import os import re from sys import exit -import ipaddress - from ipaddress import ip_address from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge +from vyos.configverify import verify_vrf from vyos.ifconfig import Section -from vyos.ifconfig import Interface from vyos.template import render from vyos.util import call from vyos.util import cmd @@ -194,6 +192,7 @@ def verify(flow_config): sflow_collector_ipver = ip_address(server).version # check if vrf is defined for Sflow + verify_vrf(flow_config) sflow_vrf = None if 'vrf' in flow_config: sflow_vrf = flow_config['vrf'] @@ -211,7 +210,7 @@ def verify(flow_config): if not is_addr_assigned(tmp, sflow_vrf): raise ConfigError(f'Configured "sflow agent-address {tmp}" does not exist in the system!') - # Check if configured netflow source-address exist in the system + # Check if configured sflow source-address exist in the system if 'source_address' in flow_config['sflow']: if not is_addr_assigned(flow_config['sflow']['source_address'], sflow_vrf): tmp = flow_config['sflow']['source_address'] @@ -219,13 +218,18 @@ def verify(flow_config): # check NetFlow configuration if 'netflow' in flow_config: + # check if vrf is defined for netflow + netflow_vrf = None + if 'vrf' in flow_config: + netflow_vrf = flow_config['vrf'] + # check if at least one NetFlow collector is configured if NetFlow configuration is presented if 'server' not in flow_config['netflow']: raise ConfigError('You need to configure at least one NetFlow server!') # Check if configured netflow source-address exist in the system if 'source_address' in flow_config['netflow']: - if not is_addr_assigned(flow_config['netflow']['source_address']): + if not is_addr_assigned(flow_config['netflow']['source_address'], netflow_vrf): tmp = flow_config['netflow']['source_address'] raise ConfigError(f'Configured "netflow source-address {tmp}" does not exist on the system!') diff --git a/src/conf_mode/interfaces-bridge.py b/src/conf_mode/interfaces-bridge.py index b961408db..4da3b097f 100755 --- a/src/conf_mode/interfaces-bridge.py +++ b/src/conf_mode/interfaces-bridge.py @@ -131,11 +131,11 @@ def verify(bridge): raise ConfigError('Loopback interface "lo" can not be added to a bridge') if 'is_bridge_member' in interface_config: - tmp = interface_config['is_bridge_member'] + tmp = next(iter(interface_config['is_bridge_member'])) raise ConfigError(error_msg + f'it is already a member of bridge "{tmp}"!') if 'is_bond_member' in interface_config: - tmp = interface_config['is_bond_member'] + tmp = next(iter(interface_config['is_bond_member'])) raise ConfigError(error_msg + f'it is already a member of bond "{tmp}"!') if 'is_source_interface' in interface_config: diff --git a/src/conf_mode/netns.py b/src/conf_mode/netns.py index 0924eb616..20129ce65 100755 --- a/src/conf_mode/netns.py +++ b/src/conf_mode/netns.py @@ -82,7 +82,8 @@ def verify(netns): if 'name' in netns: for name, config in netns['name'].items(): - print(name) + # no tests (yet) + pass return None diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index 1b8377a4a..1dd973d67 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -93,6 +93,10 @@ def verify(rtradv): if not (int(valid_lifetime) >= int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime') + if 'name_server' in interface_config: + if len(interface_config['name_server']) > 3: + raise ConfigError('No more then 3 IPv6 name-servers supported!') + if 'name_server_lifetime' in interface_config: # man page states: # The maximum duration how long the RDNSS entries are used for name diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 63887b278..b82d90e4d 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -455,7 +455,7 @@ def verify(ipsec): if dict_search('options.disable_route_autoinstall', ipsec) == None: - Warning('It\'s recommended to use ipsec vty with the next command\n[set vpn ipsec option disable-route-autoinstall]') + Warning('It\'s recommended to use ipsec vti with the next command\n[set vpn ipsec option disable-route-autoinstall]') if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] diff --git a/src/conf_mode/vpn_openconnect.py b/src/conf_mode/vpn_openconnect.py index 83021a3e6..3d5dc12a4 100755 --- a/src/conf_mode/vpn_openconnect.py +++ b/src/conf_mode/vpn_openconnect.py @@ -30,7 +30,7 @@ from vyos.util import is_listen_port_bind_service from vyos.util import dict_search from vyos.xml import defaults from vyos import ConfigError -from crypt import crypt, mksalt, METHOD_SHA512 +from passlib.hash import sha512_crypt from time import sleep from vyos import airbag @@ -45,7 +45,8 @@ radius_servers = cfg_dir + '/radius_servers' # Generate hash from user cleartext password def get_hash(password): - return crypt(password, mksalt(METHOD_SHA512)) + return sha512_crypt.hash(password) + def _default_dict_cleanup(origin: dict, default_values: dict) -> dict: diff --git a/src/etc/skel/.bashrc b/src/etc/skel/.bashrc new file mode 100644 index 000000000..ba7d50003 --- /dev/null +++ b/src/etc/skel/.bashrc @@ -0,0 +1,119 @@ +# ~/.bashrc: executed by bash(1) for non-login shells. +# see /usr/share/doc/bash/examples/startup-files (in the package bash-doc) +# for examples + +# If not running interactively, don't do anything +case $- in + *i*) ;; + *) return;; +esac + +# don't put duplicate lines or lines starting with space in the history. +# See bash(1) for more options +HISTCONTROL=ignoreboth + +# append to the history file, don't overwrite it +shopt -s histappend + +# for setting history length see HISTSIZE and HISTFILESIZE in bash(1) +HISTSIZE=1000 +HISTFILESIZE=2000 + +# check the window size after each command and, if necessary, +# update the values of LINES and COLUMNS. +shopt -s checkwinsize + +# If set, the pattern "**" used in a pathname expansion context will +# match all files and zero or more directories and subdirectories. +#shopt -s globstar + +# make less more friendly for non-text input files, see lesspipe(1) +#[ -x /usr/bin/lesspipe ] && eval "$(SHELL=/bin/sh lesspipe)" + +# set variable identifying the chroot you work in (used in the prompt below) +if [ -z "${debian_chroot:-}" ] && [ -r /etc/debian_chroot ]; then + debian_chroot=$(cat /etc/debian_chroot) +fi + +# set a fancy prompt (non-color, unless we know we "want" color) +case "$TERM" in + xterm-color) color_prompt=yes;; +esac + +# uncomment for a colored prompt, if the terminal has the capability; turned +# off by default to not distract the user: the focus in a terminal window +# should be on the output of commands, not on the prompt +#force_color_prompt=yes + +if [ -n "$force_color_prompt" ]; then + if [ -x /usr/bin/tput ] && tput setaf 1 >&/dev/null; then + # We have color support; assume it's compliant with Ecma-48 + # (ISO/IEC-6429). (Lack of such support is extremely rare, and such + # a case would tend to support setf rather than setaf.) + color_prompt=yes + else + color_prompt= + fi +fi + +if [ "$color_prompt" = yes ]; then + PS1='${debian_chroot:+($debian_chroot)}\[\033[01;32m\]\u@\H${VRF:+(vrf:$VRF)}${NETNS:+(ns:$NETNS)}\[\033[00m\]:\[\033[01;34m\]\w\[\033[00m\]\$ ' +else + PS1='${debian_chroot:+($debian_chroot)}\u@\H${VRF:+:$VRF}${NETNS:+(ns:$NETNS)}:\w\$ ' +fi +unset color_prompt force_color_prompt + +# If this is an xterm set the title to user@host:dir +case "$TERM" in +xterm*|rxvt*) + PS1="\[\e]0;${debian_chroot:+($debian_chroot)}\u@\H: \w\a\]$PS1" + ;; +*) + ;; +esac + +# enable color support of ls and also add handy aliases +if [ -x /usr/bin/dircolors ]; then + test -r ~/.dircolors && eval "$(dircolors -b ~/.dircolors)" || eval "$(dircolors -b)" + alias ls='ls --color=auto' + #alias dir='dir --color=auto' + #alias vdir='vdir --color=auto' + + #alias grep='grep --color=auto' + #alias fgrep='fgrep --color=auto' + #alias egrep='egrep --color=auto' +fi + +# colored GCC warnings and errors +#export GCC_COLORS='error=01;31:warning=01;35:note=01;36:caret=01;32:locus=01:quote=01' + +# some more ls aliases +#alias ll='ls -l' +#alias la='ls -A' +#alias l='ls -CF' + +# Alias definitions. +# You may want to put all your additions into a separate file like +# ~/.bash_aliases, instead of adding them here directly. +# See /usr/share/doc/bash-doc/examples in the bash-doc package. + +if [ -f ~/.bash_aliases ]; then + . ~/.bash_aliases +fi + +# enable programmable completion features (you don't need to enable +# this, if it's already enabled in /etc/bash.bashrc and /etc/profile +# sources /etc/bash.bashrc). +if ! shopt -oq posix; then + if [ -f /usr/share/bash-completion/bash_completion ]; then + . /usr/share/bash-completion/bash_completion + elif [ -f /etc/bash_completion ]; then + . /etc/bash_completion + fi +fi +OPAMROOT='/opt/opam'; export OPAMROOT; +OPAM_SWITCH_PREFIX='/opt/opam/4.07.0'; export OPAM_SWITCH_PREFIX; +CAML_LD_LIBRARY_PATH='/opt/opam/4.07.0/lib/stublibs:/opt/opam/4.07.0/lib/ocaml/stublibs:/opt/opam/4.07.0/lib/ocaml'; export CAML_LD_LIBRARY_PATH; +OCAML_TOPLEVEL_PATH='/opt/opam/4.07.0/lib/toplevel'; export OCAML_TOPLEVEL_PATH; +MANPATH=':/opt/opam/4.07.0/man'; export MANPATH; +PATH='/opt/opam/4.07.0/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin'; export PATH; diff --git a/src/etc/skel/.profile b/src/etc/skel/.profile new file mode 100644 index 000000000..c9db45918 --- /dev/null +++ b/src/etc/skel/.profile @@ -0,0 +1,22 @@ +# ~/.profile: executed by the command interpreter for login shells. +# This file is not read by bash(1), if ~/.bash_profile or ~/.bash_login +# exists. +# see /usr/share/doc/bash/examples/startup-files for examples. +# the files are located in the bash-doc package. + +# the default umask is set in /etc/profile; for setting the umask +# for ssh logins, install and configure the libpam-umask package. +#umask 022 + +# if running bash +if [ -n "$BASH_VERSION" ]; then + # include .bashrc if it exists + if [ -f "$HOME/.bashrc" ]; then + . "$HOME/.bashrc" + fi +fi + +# set PATH so it includes user's private bin if it exists +if [ -d "$HOME/bin" ] ; then + PATH="$HOME/bin:$PATH" +fi diff --git a/src/etc/systemd/system/ddclient.service.d/override.conf b/src/etc/systemd/system/ddclient.service.d/override.conf deleted file mode 100644 index 09d929d39..000000000 --- a/src/etc/systemd/system/ddclient.service.d/override.conf +++ /dev/null @@ -1,11 +0,0 @@ -[Unit] -After= -After=vyos-router.service - -[Service] -WorkingDirectory= -WorkingDirectory=/run/ddclient -PIDFile= -PIDFile=/run/ddclient/ddclient.pid -ExecStart= -ExecStart=/usr/bin/ddclient -cache /run/ddclient/ddclient.cache -pid /run/ddclient/ddclient.pid -file /run/ddclient/ddclient.conf diff --git a/src/etc/systemd/system/radvd.service.d/override.conf b/src/etc/systemd/system/radvd.service.d/override.conf index 472710a8b..812446dd9 100644 --- a/src/etc/systemd/system/radvd.service.d/override.conf +++ b/src/etc/systemd/system/radvd.service.d/override.conf @@ -16,3 +16,4 @@ ExecReload=/usr/sbin/radvd --logmethod stderr_clean --configtest --config /run/r ExecReload=/bin/kill -HUP $MAINPID PIDFile= PIDFile=/run/radvd/radvd.pid +Restart=always diff --git a/src/helpers/commit-confirm-notify.py b/src/helpers/commit-confirm-notify.py index eb7859ffa..8d7626c78 100755 --- a/src/helpers/commit-confirm-notify.py +++ b/src/helpers/commit-confirm-notify.py @@ -17,6 +17,7 @@ def notify(interval): if __name__ == "__main__": # Must be run as root to call wall(1) without a banner. if len(sys.argv) != 2 or os.getuid() != 0: + print('This script requires superuser privileges.', file=sys.stderr) exit(1) minutes = int(sys.argv[1]) # Drop the argument from the list so that the notification diff --git a/src/migration-scripts/dns-dynamic/0-to-1 b/src/migration-scripts/dns-dynamic/0-to-1 new file mode 100755 index 000000000..cf0983b01 --- /dev/null +++ b/src/migration-scripts/dns-dynamic/0-to-1 @@ -0,0 +1,104 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2023 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/>. + +# T5144: +# - migrate "service dns dynamic interface ..." +# to "service dns dynamic address ..." +# - migrate "service dns dynamic interface <interface> use-web ..." +# to "service dns dynamic address <address> web-options ..." +# - migrate "service dns dynamic interface <interface> rfc2136 <config> record ..." +# to "service dns dynamic address <address> rfc2136 <config> host-name ..." +# - migrate "service dns dynamic interface <interface> service <config> login ..." +# to "service dns dynamic address <address> service <config> username ..." +# - apply global 'ipv6-enable' to per <config> 'ip-version: ipv6' +# - apply service protocol mapping upfront, they are not 'auto-detected' anymore + +import sys +from vyos.configtree import ConfigTree + +service_protocol_mapping = { + 'afraid': 'freedns', + 'changeip': 'changeip', + 'cloudflare': 'cloudflare', + 'dnspark': 'dnspark', + 'dslreports': 'dslreports1', + 'dyndns': 'dyndns2', + 'easydns': 'easydns', + 'namecheap': 'namecheap', + 'noip': 'noip', + 'sitelutions': 'sitelutions', + 'zoneedit': 'zoneedit1' +} + +if (len(sys.argv) < 1): + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +old_base_path = ['service', 'dns', 'dynamic', 'interface'] +new_base_path = ['service', 'dns', 'dynamic', 'address'] + +if not config.exists(old_base_path): + # Nothing to do + sys.exit(0) + +# Migrate "service dns dynamic interface" +# to "service dns dynamic address" +config.rename(old_base_path, new_base_path[-1]) + +for address in config.list_nodes(new_base_path): + # Migrate "service dns dynamic interface <interface> rfc2136 <config> record" + # to "service dns dynamic address <address> rfc2136 <config> host-name" + if config.exists(new_base_path + [address, 'rfc2136']): + for rfc_cfg in config.list_nodes(new_base_path + [address, 'rfc2136']): + if config.exists(new_base_path + [address, 'rfc2136', rfc_cfg, 'record']): + config.rename(new_base_path + [address, 'rfc2136', rfc_cfg, 'record'], 'host-name') + + # Migrate "service dns dynamic interface <interface> service <config> login" + # to "service dns dynamic address <address> service <config> username" + if config.exists(new_base_path + [address, 'service']): + for svc_cfg in config.list_nodes(new_base_path + [address, 'service']): + if config.exists(new_base_path + [address, 'service', svc_cfg, 'login']): + config.rename(new_base_path + [address, 'service', svc_cfg, 'login'], 'username') + # Apply global 'ipv6-enable' to per <config> 'ip-version: ipv6' + if config.exists(new_base_path + [address, 'ipv6-enable']): + config.set(new_base_path + [address, 'service', svc_cfg, 'ip-version'], + value='ipv6', replace=False) + config.delete(new_base_path + [address, 'ipv6-enable']) + # Apply service protocol mapping upfront, they are not 'auto-detected' anymore + if svc_cfg in service_protocol_mapping: + config.set(new_base_path + [address, 'service', svc_cfg, 'protocol'], + value=service_protocol_mapping.get(svc_cfg), replace=False) + + # Migrate "service dns dynamic interface <interface> use-web" + # to "service dns dynamic address <address> web-options" + # Also, rename <address> to 'web' literal for backward compatibility + if config.exists(new_base_path + [address, 'use-web']): + config.rename(new_base_path + [address], 'web') + config.rename(new_base_path + ['web', 'use-web'], 'web-options') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + sys.exit(1) diff --git a/src/migration-scripts/openconnect/1-to-2 b/src/migration-scripts/openconnect/1-to-2 index 7031fb252..51cd6bc37 100755 --- a/src/migration-scripts/openconnect/1-to-2 +++ b/src/migration-scripts/openconnect/1-to-2 @@ -39,13 +39,13 @@ if not config.exists(cfg_base): else: if config.exists(cfg_base + ['authentication', 'mode']): if config.return_value(cfg_base + ['authentication', 'mode']) == 'radius': - # if "mode value radius", change to "tag node mode + valueless node radius" - config.delete(cfg_base + ['authentication','mode', 'radius']) - config.set(cfg_base + ['authentication', 'mode', 'radius'], value=None, replace=True) - elif not config.exists(cfg_base + ['authentication', 'mode', 'local']): - # if "mode local", change to "tag node mode + node local value password" - config.delete(cfg_base + ['authentication', 'mode', 'local']) - config.set(cfg_base + ['authentication', 'mode', 'local'], value='password', replace=True) + # if "mode value radius", change to "mode + valueless node radius" + config.delete_value(cfg_base + ['authentication','mode'], 'radius') + config.set(cfg_base + ['authentication', 'mode', 'radius'], value=None) + elif config.return_value(cfg_base + ['authentication', 'mode']) == 'local': + # if "mode local", change to "mode + node local value password" + config.delete_value(cfg_base + ['authentication', 'mode'], 'local') + config.set(cfg_base + ['authentication', 'mode', 'local'], value='password') try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/migration-scripts/system/18-to-19 b/src/migration-scripts/system/18-to-19 index fd0e15d42..38479d222 100755 --- a/src/migration-scripts/system/18-to-19 +++ b/src/migration-scripts/system/18-to-19 @@ -92,9 +92,6 @@ else: for intf in dhcp_interfaces: config.set(base + ['name-servers-dhcp'], value=intf, replace=False) - # delete old node - config.delete(base + ['disable-dhcp-nameservers']) - try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/op_mode/dynamic_dns.py b/src/op_mode/dns_dynamic.py index d41a74db3..76ca5249b 100755 --- a/src/op_mode/dynamic_dns.py +++ b/src/op_mode/dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2020 VyOS maintainers and contributors +# Copyright (C) 2018-2023 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 diff --git a/src/op_mode/ipsec.py b/src/op_mode/ipsec.py index db4948d7a..823bd039d 100755 --- a/src/op_mode/ipsec.py +++ b/src/op_mode/ipsec.py @@ -729,7 +729,7 @@ def _get_formatted_output_ra_summary(ra_output_list: list): sa_remotehost = sa['remote-host'] if 'remote-host' in sa else '' sa_remoteid = sa['remote-id'] if 'remote-id' in sa else '' sa_ike_proposal = _get_formatted_ike_proposal(sa) - sa_tunnel_ip = sa['remote-vips'] + sa_tunnel_ip = sa['remote-vips'][0] child_sa_key = _get_last_installed_childsa(sa) if child_sa_key: child_sa = sa['child-sas'][child_sa_key] diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index fd4f86d88..239f766fd 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018 VyOS maintainers and contributors +# Copyright (C) 2023 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 @@ -102,8 +102,18 @@ def cancel_shutdown(): else: print("Reboot or poweroff is not scheduled") +def check_unsaved_config(): + from vyos.config_mgmt import unsaved_commits + + if unsaved_commits(): + print("Warning: there are unsaved configuration changes!") + print("Run 'save' command if you do not want to lose those changes after reboot/shutdown.") + else: + pass def execute_shutdown(time, reboot=True, ask=True): + check_unsaved_config() + action = "reboot" if reboot else "poweroff" if not ask: if not ask_yes_no(f"Are you sure you want to {action} this system?"): diff --git a/src/systemd/isc-dhcp-relay6.service b/src/systemd/isc-dhcp-relay6.service index 30037e013..a365ae4b3 100644 --- a/src/systemd/isc-dhcp-relay6.service +++ b/src/systemd/isc-dhcp-relay6.service @@ -5,7 +5,7 @@ Wants=network-online.target RequiresMountsFor=/run ConditionPathExists=/run/dhcp-relay/dhcrelay6.conf After=vyos-router.service - +StartLimitIntervalSec=0 [Service] Type=forking WorkingDirectory=/run/dhcp-relay @@ -15,6 +15,6 @@ EnvironmentFile=/run/dhcp-relay/dhcrelay6.conf PIDFile=/run/dhcp-relay/dhcrelay6.pid ExecStart=/usr/sbin/dhcrelay -6 -pf /run/dhcp-relay/dhcrelay6.pid $OPTIONS Restart=always - +RestartSec=10 [Install] WantedBy=multi-user.target |