diff options
99 files changed, 3433 insertions, 1284 deletions
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index 681cf3ef9..4a1bc4011 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -7,9 +7,6 @@ "conntrack_sync": ["conntrack_sync"], "group_resync": ["conntrack", "nat", "policy-route"] }, - "http_api": { - "https": ["https"] - }, "interfaces_bonding": { "ethernet": ["interfaces-ethernet"] }, diff --git a/data/templates/accel-ppp/config_chap_secrets_radius.j2 b/data/templates/accel-ppp/config_chap_secrets_radius.j2 index a498d8186..595e3a565 100644 --- a/data/templates/accel-ppp/config_chap_secrets_radius.j2 +++ b/data/templates/accel-ppp/config_chap_secrets_radius.j2 @@ -30,7 +30,16 @@ dae-server={{ authentication.radius.dynamic_author.server }}:{{ authentication.r {% endif %} {% endif %} {# Both chap-secrets and radius block required the gw-ip-address #} -{% if gateway_address is vyos_defined %} +{% if authentication.mode is vyos_defined('local') or authentication.mode is vyos_defined('radius') %} +{% if gateway_address is vyos_defined %} +{% if server_type == 'ipoe' %} +{% for gw in gateway_address %} +{% set host_address, _ = gw.split('/') %} +gw-ip-address={{ host_address }} +{% endfor %} +{% else %} gw-ip-address={{ gateway_address }} +{% endif %} +{% endif %} {% endif %} diff --git a/data/templates/accel-ppp/config_ip_pool.j2 b/data/templates/accel-ppp/config_ip_pool.j2 index f7511e445..c567236a4 100644 --- a/data/templates/accel-ppp/config_ip_pool.j2 +++ b/data/templates/accel-ppp/config_ip_pool.j2 @@ -1,24 +1,22 @@ -{% if client_ip_pool is vyos_defined %} +{% if ordered_named_pools is vyos_defined %} [ip-pool] {% if gateway_address is vyos_defined %} +{% if server_type == 'ipoe' %} +{% for gw in gateway_address %} +{% set host_address, _ = gw.split('/') %} +gw-ip-address={{ host_address }} +{% endfor %} +{% else %} gw-ip-address={{ gateway_address }} +{% endif %} {% endif %} -{% if client_ip_pool.start is vyos_defined and client_ip_pool.stop is vyos_defined %} -{{ client_ip_pool.start }}-{{ client_ip_pool.stop.split('.')[3] }} -{% endif %} -{% if client_ip_pool.subnet is vyos_defined %} -{% for subnet in client_ip_pool.subnet %} -{{ subnet }} -{% endfor %} -{% endif %} -{% if client_ip_pool.name is vyos_defined %} -{% for pool, pool_config in client_ip_pool.name.items() %} -{% if pool_config.subnet is vyos_defined %} -{{ pool_config.subnet }},name={{ pool }} -{% endif %} -{% if pool_config.gateway_address is vyos_defined %} -gw-ip-address={{ pool_config.gateway_address }} +{% for pool in ordered_named_pools %} +{% for pool_name, pool_config in pool.items() %} +{% if pool_config.next_pool is vyos_defined %} +{{ pool_config.range }},name={{ pool_name }},next={{ pool_config.next_pool }} +{% else %} +{{ pool_config.range }},name={{ pool_name }} {% endif %} {% endfor %} -{% endif %} +{% endfor %} {% endif %}
\ No newline at end of file diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 555a033d3..588f3d462 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -55,38 +55,18 @@ noauth=1 username=ifname password=csid {% endif %} -{% if client_ip_pool.name is vyos_defined %} -{% if first_named_pool is vyos_defined %} -ip-pool={{ first_named_pool }} -{% else %} -{% for pool, pool_options in client_ip_pool.name.items() %} -{% if pool_options.subnet is vyos_defined %} -ip-pool={{ pool }} -{% endif %} -{% endfor %} -{% endif %} -{% for pool, pool_options in client_ip_pool.name.items() %} -{% if pool_options.gateway_address is vyos_defined %} -gw-ip-address={{ pool_options.gateway_address }}/{{ pool_options.subnet.split('/')[1] }} -{% endif %} +{% if default_pool is vyos_defined %} +ip-pool={{ default_pool }} +{% endif %} +{% if gateway_address is vyos_defined %} +{% for gw_addr in gateway_address %} +gw-ip-address={{ gw_addr }} {% endfor %} {% endif %} proxy-arp=1 -{% if ordered_named_pools is vyos_defined %} -[ip-pool] -{% for p in ordered_named_pools %} -{% for pool, pool_options in p.items() %} -{% set next_named_pool = ',next=' ~ pool_options.next_pool if pool_options.next_pool is vyos_defined else '' %} -{{ pool_options.subnet }},name={{ pool }}{{ next_named_pool }} -{% endfor %} -{% endfor %} -{% for p in ordered_named_pools %} -{% for pool, pool_options in p.items() %} -gw-ip-address={{ pool_options.gateway_address }}/{{ pool_options.subnet.split('/')[1] }} -{% endfor %} -{% endfor %} -{% endif %} +{# Common IP pool definitions #} +{% include 'accel-ppp/config_ip_pool.j2' %} {# Common IPv6 pool definitions #} {% include 'accel-ppp/config_ipv6_pool.j2' %} diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index b089d3e71..89cc0eae7 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -6,11 +6,9 @@ chap-secrets {% for proto in auth_proto %} {{ proto }} {% endfor %} - {% if auth_mode == 'radius' %} radius {% endif %} - ippool shaper ipv6pool @@ -65,24 +63,15 @@ secret={{ lns_shared_secret }} {% if lns_host_name %} host-name={{ lns_host_name }} {% endif %} +{% if default_pool is vyos_defined %} +ip-pool={{ default_pool }} +{% endif %} [client-ip-range] 0.0.0.0/0 -{% if client_ip_pool or client_ip_subnets %} -[ip-pool] -{% if client_ip_pool %} -{{ client_ip_pool }} -{% endif %} -{% if client_ip_subnets %} -{% for sn in client_ip_subnets %} -{{ sn }} -{% endfor %} -{% endif %} -{% endif %} -{% if gateway_address %} -gw-ip-address={{ gateway_address }} -{% endif %} +{# Common IP pool definitions #} +{% include 'accel-ppp/config_ip_pool.j2' %} {% if auth_mode == 'local' %} [chap-secrets] @@ -115,7 +104,7 @@ nas-ip-address={{ radius_nas_ip }} bind={{ radius_source_address }} {% endif %} {% endif %} -{% if gateway_address %} +{% if gateway_address is vyos_defined %} gw-ip-address={{ gateway_address }} {% endif %} diff --git a/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2 index e1ae3660e..4bb1c4450 100644 --- a/data/templates/accel-ppp/pppoe.config.j2 +++ b/data/templates/accel-ppp/pppoe.config.j2 @@ -108,7 +108,6 @@ unit-cache={{ ppp_options.interface_cache }} [pppoe] verbose=1 ac-name={{ access_concentrator }} - {% if interface is vyos_defined %} {% for iface, iface_config in interface.items() %} {% if iface_config.vlan is not vyos_defined %} @@ -121,11 +120,9 @@ vlan-mon={{ iface }},{{ iface_config.vlan | join(',') }} {% endif %} {% endfor %} {% endif %} - {% if service_name %} service-name={{ service_name | join(',') }} {% endif %} - {% if pado_delay %} {% set pado_delay_param = namespace(value='0') %} {% for delay in pado_delay | sort(attribute='0') %} @@ -140,21 +137,11 @@ pado-delay={{ pado_delay_param.value }} {% if authentication.radius.called_sid_format is vyos_defined %} called-sid={{ authentication.radius.called_sid_format }} {% endif %} - -{% if authentication.mode is vyos_defined("local") or authentication.mode is vyos_defined("noauth") %} -{% if authentication.mode is vyos_defined("noauth") %} +{% if authentication.mode is vyos_defined("noauth") %} noauth=1 -{% endif %} -{% if client_ip_pool.name is vyos_defined %} -{% for pool, pool_config in client_ip_pool.name.items() %} -{% if pool_config.subnet is vyos_defined %} -ip-pool={{ pool }} -{% endif %} -{% if pool_config.gateway_address is vyos_defined %} -gw-ip-address={{ pool_config.gateway_address }}/{{ pool_config.subnet.split('/')[1] }} -{% endif %} -{% endfor %} -{% endif %} +{% endif %} +{% if default_pool is vyos_defined %} +ip-pool={{ default_pool }} {% endif %} {% if limits is vyos_defined %} diff --git a/data/templates/accel-ppp/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2 index 46a9f933a..4e891777f 100644 --- a/data/templates/accel-ppp/pptp.config.j2 +++ b/data/templates/accel-ppp/pptp.config.j2 @@ -51,14 +51,15 @@ ppp-max-mtu={{ mtu }} mppe={{ ppp_mppe }} echo-interval=10 echo-failure=3 - +{% if default_pool is vyos_defined %} +ip-pool={{ default_pool }} +{% endif %} [client-ip-range] 0.0.0.0/0 -[ip-pool] -tunnel={{ client_ip_pool }} -gw-ip-address={{ gw_ip }} +{# Common IP pool definitions #} +{% include 'accel-ppp/config_ip_pool.j2' %} [ppp] verbose=5 @@ -74,18 +75,15 @@ verbose=1 {% for r in radius_server %} server={{ r.server }},{{ r.key }},auth-port={{ r.port }},acct-port={{ r.acct_port }},req-limit=0,fail-time={{ r.fail_time }} {% endfor %} - {% if radius_acct_interim_interval is vyos_defined %} acct-interim-interval={{ radius_acct_interim_interval }} {% endif %} {% if radius_acct_inter_jitter %} acct-interim-jitter={{ radius_acct_inter_jitter }} {% endif %} - acct-timeout={{ radius_acct_tmo }} timeout={{ radius_timeout }} max-try={{ radius_max_try }} - {% if radius_nas_id %} nas-identifier={{ radius_nas_id }} {% endif %} @@ -97,8 +95,8 @@ bind={{ radius_source_address }} {% endif %} {% endif %} {# Both chap-secrets and radius block required the gw-ip-address #} -{% if gw_ip is defined and gw_ip is not none %} -gw-ip-address={{ gw_ip }} +{% if gateway_address is vyos_defined %} +gw-ip-address={{ gateway_address }} {% endif %} {% if radius_shaper_enable %} diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index cf1d23f54..6117cea1b 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -36,6 +36,9 @@ accept=ssl ssl-ca-file=/run/accel-pppd/sstp-ca.pem ssl-pemfile=/run/accel-pppd/sstp-cert.pem ssl-keyfile=/run/accel-pppd/sstp-cert.key +{% if default_pool is vyos_defined %} +ip-pool={{ default_pool }} +{% endif %} {# Common IP pool definitions #} {% include 'accel-ppp/config_ip_pool.j2' %} @@ -56,7 +59,6 @@ check-ip=1 mtu={{ mtu }} ipv6={{ 'allow' if ppp_options.ipv6 is vyos_defined("deny") and client_ipv6_pool is vyos_defined else ppp_options.ipv6 }} ipv4={{ ppp_options.ipv4 }} - mppe={{ ppp_options.mppe }} lcp-echo-interval={{ ppp_options.lcp_echo_interval }} lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }} diff --git a/data/templates/conntrack/nftables-ct.j2 b/data/templates/conntrack/nftables-ct.j2 index 1e0fc8065..762a6f693 100644 --- a/data/templates/conntrack/nftables-ct.j2 +++ b/data/templates/conntrack/nftables-ct.j2 @@ -11,20 +11,33 @@ table ip vyos_conntrack { {% if ignore.ipv4.rule is vyos_defined %} {% for rule, rule_config in ignore.ipv4.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} - {{ rule_config | conntrack_ignore_rule(rule, ipv6=False) }} + {{ rule_config | conntrack_rule(rule, 'ignore', ipv6=False) }} {% endfor %} {% endif %} - return + return } chain VYOS_CT_TIMEOUT { -{% if timeout.custom.rule is vyos_defined %} -{% for rule, rule_config in timeout.custom.rule.items() %} +{% if timeout.custom.ipv4.rule is vyos_defined %} +{% for rule, rule_config in timeout.custom.ipv4.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} + {{ rule_config | conntrack_rule(rule, 'timeout', ipv6=False) }} {% endfor %} {% endif %} return } +{% if timeout.custom.ipv4.rule is vyos_defined %} +{% for rule, rule_config in timeout.custom.ipv4.rule.items() %} + ct timeout ct-timeout-{{ rule }} { + l3proto ip; +{% for protocol, protocol_config in rule_config.protocol.items() %} + protocol {{ protocol }}; + policy = { {{ protocol_config | conntrack_ct_policy() }} } +{% endfor %} + } +{% endfor %} +{% endif %} + chain PREROUTING { type filter hook prerouting priority -300; policy accept; {% if ipv4_firewall_action == 'accept' or ipv4_nat_action == 'accept' %} @@ -80,20 +93,33 @@ table ip6 vyos_conntrack { {% if ignore.ipv6.rule is vyos_defined %} {% for rule, rule_config in ignore.ipv6.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} - {{ rule_config | conntrack_ignore_rule(rule, ipv6=True) }} + {{ rule_config | conntrack_rule(rule, 'ignore', ipv6=True) }} {% endfor %} {% endif %} return } chain VYOS_CT_TIMEOUT { -{% if timeout.custom.rule is vyos_defined %} -{% for rule, rule_config in timeout.custom.rule.items() %} +{% if timeout.custom.ipv6.rule is vyos_defined %} +{% for rule, rule_config in timeout.custom.ipv6.rule.items() %} # rule-{{ rule }} {{ '- ' ~ rule_config.description if rule_config.description is vyos_defined }} + {{ rule_config | conntrack_rule(rule, 'timeout', ipv6=True) }} {% endfor %} {% endif %} return } +{% if timeout.custom.ipv6.rule is vyos_defined %} +{% for rule, rule_config in timeout.custom.ipv6.rule.items() %} + ct timeout ct-timeout-{{ rule }} { + l3proto ip; +{% for protocol, protocol_config in rule_config.protocol.items() %} + protocol {{ protocol }}; + policy = { {{ protocol_config | conntrack_ct_policy() }} } +{% endfor %} + } +{% endfor %} +{% endif %} + chain PREROUTING { type filter hook prerouting priority -300; policy accept; {% if ipv6_firewall_action == 'accept' or ipv6_nat_action == 'accept' %} diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 356b8d0d0..30afb9e64 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -29,44 +29,28 @@ cache={{ config_file | replace('.conf', '.cache') }} {# ddclient default (web=dyndns) doesn't support ssl and results in process lockup #} web=googledomains {# ddclient default (use=ip) results in confusing warning message in log #} -use=disabled +use=no -{% 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 %} +{% if name is vyos_defined %} +{% for service, config in name.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 [config.ip_version[2:]] %} +{% for host in config.host_name if config.host_name is vyos_defined %} +{# ip_suffixes can be either of ['v4'], ['v6'], ['v4', 'v6'] for all protocols except 'nsupdate' + ip_suffixes must be [''] for nsupdate since it doesn't support usevX/wantipvX yet #} +{% set ip_suffixes = ['v4', 'v6'] if config.ip_version == 'both' + else ([config.ip_version[2:]] if config.protocol != 'nsupdate' + else ['']) %} +{% set password = config.key if config.protocol == 'nsupdate' + else config.password %} -# Web service dynamic DNS configuration for {{ name }}: [{{ config.protocol }}, {{ host }}] -{{ render_config(host, address, service_cfg.web_options, ip_suffixes, +# Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}] +{{ render_config(host, config.address, config.web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, - login=config.username, password=config.password, ttl=config.ttl, + login=config.username, password=password, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} -{% endfor %} -{% endfor %} -{% endif %} +{% endfor %} {% endfor %} {% endif %} diff --git a/data/templates/ocserv/ocserv_config.j2 b/data/templates/ocserv/ocserv_config.j2 index 1401b8b26..80ba357bc 100644 --- a/data/templates/ocserv/ocserv_config.j2 +++ b/data/templates/ocserv/ocserv_config.j2 @@ -119,4 +119,20 @@ split-dns = {{ tmp }} {% for grp in authentication.group %} select-group = {{ grp }} {% endfor %} -{% endif %}
\ No newline at end of file +{% endif %} + + +# HTTP security headers +included-http-headers = Strict-Transport-Security: max-age=31536000 ; includeSubDomains +included-http-headers = X-Frame-Options: deny +included-http-headers = X-Content-Type-Options: nosniff +included-http-headers = Content-Security-Policy: default-src ´none´ +included-http-headers = X-Permitted-Cross-Domain-Policies: none +included-http-headers = Referrer-Policy: no-referrer +included-http-headers = Clear-Site-Data: "cache","cookies","storage" +included-http-headers = Cross-Origin-Embedder-Policy: require-corp +included-http-headers = Cross-Origin-Opener-Policy: same-origin +included-http-headers = Cross-Origin-Resource-Policy: same-origin +included-http-headers = X-XSS-Protection: 0 +included-http-headers = Pragma: no-cache +included-http-headers = Cache-control: no-store, no-cache diff --git a/debian/control b/debian/control index b2b0c6ed0..f20268444 100644 --- a/debian/control +++ b/debian/control @@ -249,6 +249,9 @@ Depends: libstrongswan-standard-plugins (>=5.9), python3-vici (>= 5.7.2), # End "vpn ipsec" +# For "nat64" + jool, +# End "nat64" # For nat66 ndppd, # End nat66 diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst index 22b50ce2a..64c60a780 100644 --- a/debian/vyos-1x.postinst +++ b/debian/vyos-1x.postinst @@ -172,7 +172,7 @@ fi DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd /etc/default/pmacctd /etc/pmacct /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf - /etc/ntp.conf /etc/default/ssh + /etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts /etc/powerdns /etc/default/pdns-recursor /etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns" for tmp in $DELETE; do @@ -200,4 +200,3 @@ systemctl enable vyos-config-cloud-init.service # Update XML cache python3 /usr/lib/python3/dist-packages/vyos/xml_ref/update_cache.py - diff --git a/interface-definitions/dns-dynamic.xml.in b/interface-definitions/dns-dynamic.xml.in index 32c5af9b6..f089f0e52 100644 --- a/interface-definitions/dns-dynamic.xml.in +++ b/interface-definitions/dns-dynamic.xml.in @@ -12,27 +12,48 @@ <help>Dynamic DNS</help> </properties> <children> - <tagNode name="address"> + <tagNode name="name"> <properties> - <help>Obtain IP address to send Dynamic DNS update for</help> + <help>Dynamic DNS configuration</help> <valueHelp> <format>txt</format> - <description>Use interface to obtain the IP address</description> + <description>Dynamic DNS service name</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> + #include <include/generic-description.xml.i> + <leafNode name="protocol"> + <properties> + <help>ddclient protocol used for Dynamic DNS service</help> + <completionHelp> + <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script> + </completionHelp> + <constraint> + <validator name="ddclient-protocol"/> + </constraint> + </properties> + </leafNode> + <leafNode name="address"> + <properties> + <help>Obtain IP address to send Dynamic DNS update for</help> + <valueHelp> + <format>txt</format> + <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> + </leafNode> <node name="web-options"> <properties> <help>Options when using HTTP(S) web request to obtain the IP address</help> @@ -50,88 +71,117 @@ </leafNode> </children> </node> - <tagNode name="rfc2136"> + <leafNode name="ip-version"> <properties> - <help>RFC2136 nsupdate configuration</help> + <help>IP address version to use</help> <valueHelp> - <format>txt</format> - <description>RFC2136 nsupdate service name</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> - <children> - #include <include/generic-description.xml.i> - #include <include/dns/dynamic-service-host-name-server.xml.i> - #include <include/dns/dynamic-service-wait-expiry-time.xml.i> - <leafNode name="key"> - <properties> - <help>File containing the TSIG secret key shared with remote DNS server</help> - <valueHelp> - <format>filename</format> - <description>File in /config/auth directory</description> - </valueHelp> - <constraint> - <validator name="file-path" argument="--strict --parent-dir /config/auth"/> - </constraint> - </properties> - </leafNode> - #include <include/dns/time-to-live.xml.i> - #include <include/dns/dynamic-service-zone.xml.i> - </children> - </tagNode> - <tagNode name="service"> + <defaultValue>ipv4</defaultValue> + </leafNode> + <leafNode name="host-name"> + <properties> + <help>Hostname to register with Dynamic DNS service</help> + <constraint> + #include <include/constraint/host-name.xml.i> + <regex>(\@|\*)[-.A-Za-z0-9]*</regex> + </constraint> + <constraintErrorMessage>Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*'</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="server"> <properties> - <help>Dynamic DNS configuration</help> + <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> + <leafNode name="zone"> + <properties> + <help>DNS zone to be updated</help> <valueHelp> <format>txt</format> - <description>Dynamic DNS service name</description> + <description>Name of DNS zone</description> </valueHelp> + <constraint> + <validator name="fqdn"/> + </constraint> </properties> - <children> - #include <include/generic-description.xml.i> - #include <include/dns/dynamic-service-host-name-server.xml.i> - #include <include/dns/dynamic-service-wait-expiry-time.xml.i> - #include <include/generic-username.xml.i> - #include <include/generic-password.xml.i> - #include <include/dns/time-to-live.xml.i> - <leafNode name="protocol"> - <properties> - <help>ddclient protocol used for Dynamic DNS service</help> - <completionHelp> - <script>${vyos_completion_dir}/list_ddclient_protocols.sh</script> - </completionHelp> - <constraint> - <validator name="ddclient-protocol"/> - </constraint> - </properties> - </leafNode> - #include <include/dns/dynamic-service-zone.xml.i> - <leafNode name="ip-version"> - <properties> - <help>IP address version to use</help> - <valueHelp> - <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> - </children> - </tagNode> + </leafNode> + #include <include/generic-username.xml.i> + #include <include/generic-password.xml.i> + <leafNode name="key"> + <properties> + <help>File containing TSIG authentication key for RFC2136 nsupdate on remote DNS server</help> + <valueHelp> + <format>filename</format> + <description>File in /config/auth directory</description> + </valueHelp> + <constraint> + <validator name="file-path" argument="--strict --parent-dir /config/auth"/> + </constraint> + </properties> + </leafNode> + #include <include/dns/time-to-live.xml.i> + <leafNode name="wait-time"> + <properties> + <help>Time in seconds to wait between update attempts</help> + <valueHelp> + <format>u32:60-86400</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 60-86400"/> + </constraint> + <constraintErrorMessage>Wait time must be between 60 and 86400 seconds</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="expiry-time"> + <properties> + <help>Time in seconds for the hostname to be marked expired in cache</help> + <valueHelp> + <format>u32:300-2160000</format> + <description>Time in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 300-2160000"/> + </constraint> + <constraintErrorMessage>Expiry time must be between 300 and 2160000 seconds</constraintErrorMessage> + </properties> + </leafNode> </children> </tagNode> <leafNode name="interval"> diff --git a/interface-definitions/https.xml.in b/interface-definitions/https.xml.in index 05c552e6b..ca5a5f088 100644 --- a/interface-definitions/https.xml.in +++ b/interface-definitions/https.xml.in @@ -54,10 +54,9 @@ #include <include/allow-client.xml.i> </children> </tagNode> - <node name="api" owner="${vyos_conf_scripts_dir}/http-api.py"> + <node name="api"> <properties> <help>VyOS HTTP API configuration</help> - <priority>1002</priority> </properties> <children> <node name="keys"> diff --git a/interface-definitions/include/accel-ppp/client-ip-pool-name.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool-name.xml.i deleted file mode 100644 index b442a15b9..000000000 --- a/interface-definitions/include/accel-ppp/client-ip-pool-name.xml.i +++ /dev/null @@ -1,30 +0,0 @@ -<!-- include start from accel-ppp/client-ip-pool-name.xml.i --> -<tagNode name="name"> - <properties> - <help>Pool name</help> - <valueHelp> - <format>txt</format> - <description>Name of IP pool</description> - </valueHelp> - <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> - </constraint> - </properties> - <children> - #include <include/accel-ppp/gateway-address.xml.i> - #include <include/accel-ppp/client-ip-pool-subnet-single.xml.i> - <leafNode name="next-pool"> - <properties> - <help>Next pool name</help> - <valueHelp> - <format>txt</format> - <description>Name of IP pool</description> - </valueHelp> - <constraint> - <regex>[-_a-zA-Z0-9.]+</regex> - </constraint> - </properties> - </leafNode> - </children> -</tagNode> -<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/client-ip-pool-start-stop.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool-start-stop.xml.i deleted file mode 100644 index 5f4132d13..000000000 --- a/interface-definitions/include/accel-ppp/client-ip-pool-start-stop.xml.i +++ /dev/null @@ -1,18 +0,0 @@ -<!-- include start from accel-ppp/client-ip-pool-start-stop.xml.i --> -<leafNode name="start"> - <properties> - <help>First IP address in the pool</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> -</leafNode> -<leafNode name="stop"> - <properties> - <help>Last IP address in the pool</help> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i deleted file mode 100644 index b93ba67d8..000000000 --- a/interface-definitions/include/accel-ppp/client-ip-pool-subnet-single.xml.i +++ /dev/null @@ -1,16 +0,0 @@ -<!-- include start from accel-ppp/client-ip-pool-subnet-single.xml.i --> -<leafNode name="subnet"> - <properties> - <help>Client IP subnet (CIDR notation)</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - <validator name="ipv4-host"/> - </constraint> - <constraintErrorMessage>Not a valid IP address or prefix</constraintErrorMessage> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/client-ip-pool-subnet.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool-subnet.xml.i deleted file mode 100644 index 2dc71d3f9..000000000 --- a/interface-definitions/include/accel-ppp/client-ip-pool-subnet.xml.i +++ /dev/null @@ -1,16 +0,0 @@ -<!-- include start from accel-ppp/client-ip-pool-subnet.xml.i --> -<leafNode name="subnet"> - <properties> - <help>Client IP subnet (CIDR notation)</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - </constraint> - <constraintErrorMessage>Not a valid CIDR formatted prefix</constraintErrorMessage> - <multi /> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i new file mode 100644 index 000000000..dff574e6c --- /dev/null +++ b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i @@ -0,0 +1,46 @@ +<!-- include start from accel-ppp/client-ip-pool.xml.i --> +<tagNode name="client-ip-pool"> + <properties> + <help>Client IP pool</help> + <valueHelp> + <format>txt</format> + <description>Name of IP pool</description> + </valueHelp> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + </properties> + <children> + <leafNode name="range"> + <properties> + <help>Range of IP addresses</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv4range</format> + <description>IPv4 address range inside /24 network</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv4-host"/> + <validator name="ipv4-range-mask" argument="-m 24 -r"/> + </constraint> + </properties> + </leafNode> + <leafNode name="next-pool"> + <properties> + <help>Next pool name</help> + <valueHelp> + <format>txt</format> + <description>Name of IP pool</description> + </valueHelp> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + </properties> + </leafNode> + </children> +</tagNode> +<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/default-pool.xml.i b/interface-definitions/include/accel-ppp/default-pool.xml.i new file mode 100644 index 000000000..832594c12 --- /dev/null +++ b/interface-definitions/include/accel-ppp/default-pool.xml.i @@ -0,0 +1,14 @@ +<!-- include start from accel-ppp/default-pool.xml.i --> +<leafNode name="default-pool"> + <properties> + <help>Default client IP pool name</help> + <valueHelp> + <format>txt</format> + <description>Default IP pool</description> + </valueHelp> + <constraint> + <regex>[-_a-zA-Z0-9.]+</regex> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/gateway-address-multi.xml.i b/interface-definitions/include/accel-ppp/gateway-address-multi.xml.i new file mode 100644 index 000000000..dcc58b97a --- /dev/null +++ b/interface-definitions/include/accel-ppp/gateway-address-multi.xml.i @@ -0,0 +1,17 @@ +<!-- include start from accel-ppp/gateway-address-multi.xml.i --> +<leafNode name="gateway-address"> + <properties> + <help>Gateway IP address</help> + <constraintErrorMessage>invalid IPv4 address</constraintErrorMessage> + <valueHelp> + <format>ipv4net</format> + <description>Default Gateway, mask send to the client</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv4-host"/> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/conntrack/timeout-custom-protocols.xml.i b/interface-definitions/include/conntrack/timeout-custom-protocols.xml.i new file mode 100644 index 000000000..e6bff7e4d --- /dev/null +++ b/interface-definitions/include/conntrack/timeout-custom-protocols.xml.i @@ -0,0 +1,136 @@ +<!-- include start from conntrack/timeout-custom-protocols.xml.i --> +<node name="tcp"> + <properties> + <help>TCP connection timeout options</help> + </properties> + <children> + <leafNode name="close-wait"> + <properties> + <help>TCP CLOSE-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP CLOSE-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="close"> + <properties> + <help>TCP CLOSE timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP CLOSE timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="established"> + <properties> + <help>TCP ESTABLISHED timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP ESTABLISHED timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="fin-wait"> + <properties> + <help>TCP FIN-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP FIN-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="last-ack"> + <properties> + <help>TCP LAST-ACK timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP LAST-ACK timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="syn-recv"> + <properties> + <help>TCP SYN-RECEIVED timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP SYN-RECEIVED timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="syn-sent"> + <properties> + <help>TCP SYN-SENT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP SYN-SENT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time-wait"> + <properties> + <help>TCP TIME-WAIT timeout in seconds</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>TCP TIME-WAIT timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<node name="udp"> + <properties> + <help>UDP timeout options</help> + </properties> + <children> + <leafNode name="replied"> + <properties> + <help>Timeout for UDP connection seen in both directions</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>Timeout for UDP connection seen in both directions</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + <leafNode name="unreplied"> + <properties> + <help>Timeout for unreplied UDP</help> + <valueHelp> + <format>u32:1-21474836</format> + <description>Timeout for unreplied UDP</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-21474836"/> + </constraint> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> 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 deleted file mode 100644 index 9dd14f97c..000000000 --- a/interface-definitions/include/dns/dynamic-service-host-name-server.xml.i +++ /dev/null @@ -1,35 +0,0 @@ -<!-- 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> - <regex>(\@|\*)[-.A-Za-z0-9]*</regex> - </constraint> - <constraintErrorMessage>Host-name must be alphanumeric, can contain hyphens and can be prefixed with '@' or '*'</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/dns/dynamic-service-wait-expiry-time.xml.i b/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i deleted file mode 100644 index 866690cbe..000000000 --- a/interface-definitions/include/dns/dynamic-service-wait-expiry-time.xml.i +++ /dev/null @@ -1,28 +0,0 @@ -<!-- include start from dns/dynamic-service-wait-expiry-time.xml.i --> -<leafNode name="wait-time"> - <properties> - <help>Time in seconds to wait between update attempts</help> - <valueHelp> - <format>u32:60-86400</format> - <description>Time in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 60-86400"/> - </constraint> - <constraintErrorMessage>Wait time must be between 60 and 86400 seconds</constraintErrorMessage> - </properties> -</leafNode> -<leafNode name="expiry-time"> - <properties> - <help>Time in seconds for the hostname to be marked expired in cache</help> - <valueHelp> - <format>u32:300-2160000</format> - <description>Time in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 300-2160000"/> - </constraint> - <constraintErrorMessage>Expiry time must be between 300 and 2160000 seconds</constraintErrorMessage> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/dns/dynamic-service-zone.xml.i b/interface-definitions/include/dns/dynamic-service-zone.xml.i deleted file mode 100644 index 0cc00468f..000000000 --- a/interface-definitions/include/dns/dynamic-service-zone.xml.i +++ /dev/null @@ -1,14 +0,0 @@ -<!-- include start from dns/dynamic-service-zone.xml.i --> -<leafNode name="zone"> - <properties> - <help>DNS zone to be updated</help> - <valueHelp> - <format>txt</format> - <description>Name of DNS zone</description> - </valueHelp> - <constraint> - <validator name="fqdn"/> - </constraint> - </properties> -</leafNode> -<!-- include end --> diff --git a/interface-definitions/include/nat64/protocol.xml.i b/interface-definitions/include/nat64/protocol.xml.i new file mode 100644 index 000000000..a640873b5 --- /dev/null +++ b/interface-definitions/include/nat64/protocol.xml.i @@ -0,0 +1,27 @@ +<!-- include start from nat64/protocol.xml.i --> +<node name="protocol"> + <properties> + <help>Apply translation address to a specfic protocol</help> + </properties> + <children> + <leafNode name="tcp"> + <properties> + <help>Transmission Control Protocol</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="udp"> + <properties> + <help>User Datagram Protocol</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="icmp"> + <properties> + <help>Internet Control Message Protocol</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/version/dns-dynamic-version.xml.i b/interface-definitions/include/version/dns-dynamic-version.xml.i index 7bdb90a35..773a6ab51 100644 --- a/interface-definitions/include/version/dns-dynamic-version.xml.i +++ b/interface-definitions/include/version/dns-dynamic-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dns-dynamic-version.xml.i --> -<syntaxVersion component='dns-dynamic' version='2'></syntaxVersion> +<syntaxVersion component='dns-dynamic' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/ipoe-server-version.xml.i b/interface-definitions/include/version/ipoe-server-version.xml.i index 00d2544e6..e5983ab39 100644 --- a/interface-definitions/include/version/ipoe-server-version.xml.i +++ b/interface-definitions/include/version/ipoe-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ipoe-server-version.xml.i --> -<syntaxVersion component='ipoe-server' version='1'></syntaxVersion> +<syntaxVersion component='ipoe-server' version='2'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/l2tp-version.xml.i b/interface-definitions/include/version/l2tp-version.xml.i index 86114d676..89edb160c 100644 --- a/interface-definitions/include/version/l2tp-version.xml.i +++ b/interface-definitions/include/version/l2tp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/l2tp-version.xml.i --> -<syntaxVersion component='l2tp' version='4'></syntaxVersion> +<syntaxVersion component='l2tp' version='5'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/pppoe-server-version.xml.i b/interface-definitions/include/version/pppoe-server-version.xml.i index 6bdd8d75c..deed702f0 100644 --- a/interface-definitions/include/version/pppoe-server-version.xml.i +++ b/interface-definitions/include/version/pppoe-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/pppoe-server-version.xml.i --> -<syntaxVersion component='pppoe-server' version='6'></syntaxVersion> +<syntaxVersion component='pppoe-server' version='7'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/pptp-version.xml.i b/interface-definitions/include/version/pptp-version.xml.i index 0296c44e9..4386cedbd 100644 --- a/interface-definitions/include/version/pptp-version.xml.i +++ b/interface-definitions/include/version/pptp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/pptp-version.xml.i --> -<syntaxVersion component='pptp' version='2'></syntaxVersion> +<syntaxVersion component='pptp' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/sstp-version.xml.i b/interface-definitions/include/version/sstp-version.xml.i index 79b43a3e7..3ac54a3de 100644 --- a/interface-definitions/include/version/sstp-version.xml.i +++ b/interface-definitions/include/version/sstp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/sstp-version.xml.i --> -<syntaxVersion component='sstp' version='4'></syntaxVersion> +<syntaxVersion component='sstp' version='5'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/nat64.xml.in b/interface-definitions/nat64.xml.in new file mode 100644 index 000000000..baf13e6cb --- /dev/null +++ b/interface-definitions/nat64.xml.in @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="nat64" owner="${vyos_conf_scripts_dir}/nat64.py"> + <properties> + <help>IPv6-to-IPv4 Network Address Translation (NAT64) Settings</help> + <priority>501</priority> + </properties> + <children> + <node name="source"> + <properties> + <help>IPv6 source to IPv4 destination address translation</help> + </properties> + <children> + <tagNode name="rule"> + <properties> + <help>Source NAT64 rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number for this rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT64 rule number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/generic-disable-node.xml.i> + <node name="source"> + <properties> + <help>IPv6 source prefix options</help> + </properties> + <children> + <leafNode name="prefix"> + <properties> + <help>IPv6 prefix to be translated</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + <node name="translation"> + <properties> + <help>Translated IPv4 address options</help> + </properties> + <children> + <tagNode name="pool"> + <properties> + <help>Translation IPv4 pool number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number for this rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>NAT64 pool number must be between 1 and 999999</constraintErrorMessage> + </properties> + <children> + #include <include/generic-description.xml.i> + #include <include/generic-disable-node.xml.i> + #include <include/nat-translation-port.xml.i> + #include <include/nat64/protocol.xml.i> + <leafNode name="address"> + <properties> + <help>IPv4 address or prefix to translate to</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode> + </children> + </node> + </children> + </tagNode> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index c470cfdb3..0d2ed9746 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -1446,9 +1446,18 @@ <format>u32:0-4294967295</format> <description>Metric value</description> </valueHelp> + <valueHelp> + <format><+/-rtt></format> + <description>Add or subtract round trip time</description> + </valueHelp> + <valueHelp> + <format><rtt></format> + <description>Round trip time</description> + </valueHelp> <constraint> <validator name="numeric" argument="--relative --"/> <validator name="numeric" argument="--range 0-4294967295"/> + <regex>^[+|-]?rtt$</regex> </constraint> </properties> </leafNode> diff --git a/interface-definitions/service-ipoe-server.xml.in b/interface-definitions/service-ipoe-server.xml.in index 9ac0c8fdf..edfe6a34c 100644 --- a/interface-definitions/service-ipoe-server.xml.in +++ b/interface-definitions/service-ipoe-server.xml.in @@ -104,14 +104,8 @@ </tagNode> #include <include/accel-ppp/max-concurrent-sessions.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> - <node name="client-ip-pool"> - <properties> - <help>Client IP pools and gateway setting</help> - </properties> - <children> - #include <include/accel-ppp/client-ip-pool-name.xml.i> - </children> - </node> + #include <include/accel-ppp/client-ip-pool.xml.i> + #include <include/accel-ppp/gateway-address-multi.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> <node name="authentication"> <properties> @@ -188,6 +182,7 @@ #include <include/accel-ppp/radius-additions.xml.i> </children> </node> + #include <include/accel-ppp/default-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/service-pppoe-server.xml.in b/interface-definitions/service-pppoe-server.xml.in index 44b689fe1..f1b369936 100644 --- a/interface-definitions/service-pppoe-server.xml.in +++ b/interface-definitions/service-pppoe-server.xml.in @@ -49,16 +49,7 @@ </node> </children> </node> - <node name="client-ip-pool"> - <properties> - <help>Pool of client IP addresses (must be within a /24)</help> - </properties> - <children> - #include <include/accel-ppp/client-ip-pool-start-stop.xml.i> - #include <include/accel-ppp/client-ip-pool-subnet.xml.i> - #include <include/accel-ppp/client-ip-pool-name.xml.i> - </children> - </node> + #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> <tagNode name="interface"> @@ -282,6 +273,7 @@ </leafNode> </children> </node> + #include <include/accel-ppp/default-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/system-conntrack.xml.in b/interface-definitions/system-conntrack.xml.in index 4452f1a74..d9504544d 100644 --- a/interface-definitions/system-conntrack.xml.in +++ b/interface-definitions/system-conntrack.xml.in @@ -385,58 +385,122 @@ <help>Define custom timeouts per connection</help> </properties> <children> - <tagNode name="rule"> + <node name="ipv4"> <properties> - <help>Rule number</help> - <valueHelp> - <format>u32:1-999999</format> - <description>Number of conntrack rule</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-999999"/> - </constraint> - <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> + <help>IPv4 rules</help> </properties> <children> - #include <include/generic-description.xml.i> - <node name="destination"> - <properties> - <help>Destination parameters</help> - </properties> - <children> - #include <include/nat-address.xml.i> - #include <include/nat-port.xml.i> - </children> - </node> - <leafNode name="inbound-interface"> - <properties> - <help>Interface to ignore connections tracking on</help> - <completionHelp> - <list>any</list> - <script>${vyos_completion_dir}/list_interfaces</script> - </completionHelp> - </properties> - </leafNode> - #include <include/ip-protocol.xml.i> - <node name="protocol"> + <tagNode name="rule"> <properties> - <help>Customize protocol specific timers, one protocol configuration per rule</help> + <help>Rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of conntrack rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - #include <include/conntrack/timeout-common-protocols.xml.i> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + </leafNode> + <node name="protocol"> + <properties> + <help>Customize protocol specific timers, one protocol configuration per rule</help> + </properties> + <children> + #include <include/conntrack/timeout-custom-protocols.xml.i> + </children> + </node> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/nat-address.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> </children> - </node> - <node name="source"> + </tagNode> + </children> + </node> + <node name="ipv6"> + <properties> + <help>IPv6 rules</help> + </properties> + <children> + <tagNode name="rule"> <properties> - <help>Source parameters</help> + <help>Rule number</help> + <valueHelp> + <format>u32:1-999999</format> + <description>Number of conntrack rule</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-999999"/> + </constraint> + <constraintErrorMessage>Ignore rule number must be between 1 and 999999</constraintErrorMessage> </properties> <children> - #include <include/nat-address.xml.i> - #include <include/nat-port.xml.i> + #include <include/generic-description.xml.i> + <node name="destination"> + <properties> + <help>Destination parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> + <leafNode name="inbound-interface"> + <properties> + <help>Interface to ignore connections tracking on</help> + <completionHelp> + <list>any</list> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + </leafNode> + <node name="protocol"> + <properties> + <help>Customize protocol specific timers, one protocol configuration per rule</help> + </properties> + <children> + #include <include/conntrack/timeout-custom-protocols.xml.i> + </children> + </node> + <node name="source"> + <properties> + <help>Source parameters</help> + </properties> + <children> + #include <include/firewall/address-ipv6.xml.i> + #include <include/nat-port.xml.i> + </children> + </node> </children> - </node> + </tagNode> </children> - </tagNode> + </node> </children> </node> #include <include/conntrack/timeout-common-protocols.xml.i> diff --git a/interface-definitions/vpn-l2tp.xml.in b/interface-definitions/vpn-l2tp.xml.in index 60a1d323b..7980cfdf5 100644 --- a/interface-definitions/vpn-l2tp.xml.in +++ b/interface-definitions/vpn-l2tp.xml.in @@ -116,15 +116,7 @@ </children> </node> #include <include/accel-ppp/wins-server.xml.i> - <node name="client-ip-pool"> - <properties> - <help>Pool of client IP addresses (must be within a /24)</help> - </properties> - <children> - #include <include/accel-ppp/client-ip-pool-start-stop.xml.i> - #include <include/accel-ppp/client-ip-pool-subnet.xml.i> - </children> - </node> + #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/generic-description.xml.i> #include <include/dhcp-interface.xml.i> @@ -253,6 +245,7 @@ #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i> </children> </node> + #include <include/accel-ppp/default-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn-pptp.xml.in b/interface-definitions/vpn-pptp.xml.in index 964c4d21e..96f87f3e2 100644 --- a/interface-definitions/vpn-pptp.xml.in +++ b/interface-definitions/vpn-pptp.xml.in @@ -23,17 +23,10 @@ </constraint> </properties> </leafNode> + #include <include/accel-ppp/gateway-address.xml.i> #include <include/name-server-ipv4.xml.i> #include <include/accel-ppp/wins-server.xml.i> - <node name="client-ip-pool"> - <properties> - <help>Pool of client IP addresses (must be within a /24)</help> - </properties> - <children> - #include <include/accel-ppp/client-ip-pool-start-stop.xml.i> - </children> - </node> - #include <include/accel-ppp/gateway-address.xml.i> + #include <include/accel-ppp/client-ip-pool.xml.i> <node name="authentication"> <properties> <help>Authentication for remote access PPTP VPN</help> @@ -118,6 +111,7 @@ #include <include/accel-ppp/radius-additions.xml.i> </children> </node> + #include <include/accel-ppp/default-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn-sstp.xml.in b/interface-definitions/vpn-sstp.xml.in index 9c818ba60..a1b69f990 100644 --- a/interface-definitions/vpn-sstp.xml.in +++ b/interface-definitions/vpn-sstp.xml.in @@ -29,19 +29,13 @@ #include <include/interface/mtu-68-1500.xml.i> #include <include/accel-ppp/gateway-address.xml.i> #include <include/name-server-ipv4-ipv6.xml.i> - <node name="client-ip-pool"> - <properties> - <help>Client IP pools and gateway setting</help> - </properties> - <children> - #include <include/accel-ppp/client-ip-pool-subnet.xml.i> - </children> - </node> + #include <include/accel-ppp/client-ip-pool.xml.i> #include <include/accel-ppp/client-ipv6-pool.xml.i> #include <include/port-number.xml.i> <leafNode name="port"> <defaultValue>443</defaultValue> </leafNode> + #include <include/accel-ppp/default-pool.xml.i> <node name="ppp-options"> <properties> <help>PPP (Point-to-Point Protocol) settings</help> diff --git a/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in index 463b985d6..c131087be 100644 --- a/op-mode-definitions/system-image.xml.in +++ b/op-mode-definitions/system-image.xml.in @@ -17,7 +17,7 @@ <list>/path/to/vyos-image.iso "http://example.com/vyos-image.iso"</list> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image_path "${4}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image-path "${4}"</command> <children> <tagNode name="vrf"> <properties> @@ -26,7 +26,7 @@ <path>vrf name</path> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image_path "${4}" --vrf "${6}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image-path "${4}" --vrf "${6}"</command> <children> <tagNode name="username"> <properties> @@ -37,7 +37,7 @@ <properties> <help>Password to use with authentication</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image_path "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image-path "${4}" --vrf "${6}" --username "${8}" --password "${10}"</command> </tagNode> </children> </tagNode> @@ -52,7 +52,7 @@ <properties> <help>Password to use with authentication</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image_path "${4}" --username "${6}" --password "${8}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_installer.py --action add --image-path "${4}" --username "${6}" --password "${8}"</command> </tagNode> </children> </tagNode> @@ -90,7 +90,7 @@ <script>sudo ${vyos_op_scripts_dir}/image_manager.py --action list</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action set --image_name "${5}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action set --image-name "${5}"</command> </tagNode> </children> </node> @@ -134,7 +134,7 @@ <script>sudo ${vyos_op_scripts_dir}/image_manager.py --action list</script> </completionHelp> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action delete --image_name "${4}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action delete --image-name "${4}"</command> </tagNode> </children> </node> @@ -162,7 +162,7 @@ <properties> <help>A new name for an image</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action rename --image_name "${4}" --image_new_name "${6}"</command> + <command>sudo ${vyos_op_scripts_dir}/image_manager.py --action rename --image-name "${4}" --image-new-name "${6}"</command> </tagNode> </children> </tagNode> diff --git a/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py new file mode 100644 index 000000000..757d447a2 --- /dev/null +++ b/python/vyos/accel_ppp_util.py @@ -0,0 +1,193 @@ +# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library 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 +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +# The sole purpose of this module is to hold common functions used in +# all kinds of implementations to verify the CLI configuration. +# It is started by migrating the interfaces to the new get_config_dict() +# approach which will lead to a lot of code that can be reused. + +# NOTE: imports should be as local as possible to the function which +# makes use of it! + +from vyos import ConfigError +from vyos.utils.dict import dict_search + + +def get_pools_in_order(data: dict) -> list: + """Return a list of dictionaries representing pool data in the order + in which they should be allocated. Pool must be defined before we can + use it with 'next-pool' option. + + Args: + data: A dictionary of pool data, where the keys are pool names and the + values are dictionaries containing the 'subnet' key and the optional + 'next_pool' key. + + Returns: + list: A list of dictionaries + + Raises: + ValueError: If a 'next_pool' key references a pool name that + has not been defined. + ValueError: If a circular reference is found in the 'next_pool' keys. + + Example: + config_data = { + ... 'first-pool': { + ... 'next_pool': 'second-pool', + ... 'subnet': '192.0.2.0/25' + ... }, + ... 'second-pool': { + ... 'next_pool': 'third-pool', + ... 'subnet': '203.0.113.0/25' + ... }, + ... 'third-pool': { + ... 'subnet': '198.51.100.0/24' + ... }, + ... 'foo': { + ... 'subnet': '100.64.0.0/24', + ... 'next_pool': 'second-pool' + ... } + ... } + + % get_pools_in_order(config_data) + [{'third-pool': {'subnet': '198.51.100.0/24'}}, + {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}}, + {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}}, + {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}] + """ + pools = [] + unresolved_pools = {} + + for pool, pool_config in data.items(): + if "next_pool" not in pool_config or not pool_config["next_pool"]: + pools.insert(0, {pool: pool_config}) + else: + unresolved_pools[pool] = pool_config + + while unresolved_pools: + resolved_pools = [] + + for pool, pool_config in unresolved_pools.items(): + next_pool_name = pool_config["next_pool"] + + if any(p for p in pools if next_pool_name in p): + index = next( + (i for i, p in enumerate(pools) if next_pool_name in p), None + ) + pools.insert(index + 1, {pool: pool_config}) + resolved_pools.append(pool) + elif next_pool_name in unresolved_pools: + # next pool not yet resolved + pass + else: + raise ConfigError( + f"Pool '{next_pool_name}' not defined in configuration data" + ) + + if not resolved_pools: + raise ConfigError("Circular reference in configuration data") + + for pool in resolved_pools: + unresolved_pools.pop(pool) + + return pools + + +def verify_accel_ppp_base_service(config, local_users=True): + """ + Common helper function which must be used by all Accel-PPP services based + on get_config_dict() + """ + # vertify auth settings + if local_users and dict_search("authentication.mode", config) == "local": + if ( + dict_search("authentication.local_users", config) is None + or dict_search("authentication.local_users", config) == {} + ): + raise ConfigError( + "Authentication mode local requires local users to be configured!" + ) + + for user in dict_search("authentication.local_users.username", config): + user_config = config["authentication"]["local_users"]["username"][user] + + if "password" not in user_config: + raise ConfigError(f'Password required for local user "{user}"') + + if "rate_limit" in user_config: + # if up/download is set, check that both have a value + if not {"upload", "download"} <= set(user_config["rate_limit"]): + raise ConfigError( + f'User "{user}" has rate-limit configured for only one ' + "direction but both upload and download must be given!" + ) + + elif dict_search("authentication.mode", config) == "radius": + if not dict_search("authentication.radius.server", config): + raise ConfigError("RADIUS authentication requires at least one server") + + for server in dict_search("authentication.radius.server", config): + radius_config = config["authentication"]["radius"]["server"][server] + if "key" not in radius_config: + raise ConfigError(f'Missing RADIUS secret key for server "{server}"') + + if "name_server_ipv4" in config: + if len(config["name_server_ipv4"]) > 2: + raise ConfigError( + "Not more then two IPv4 DNS name-servers " "can be configured" + ) + + if "name_server_ipv6" in config: + if len(config["name_server_ipv6"]) > 3: + raise ConfigError( + "Not more then three IPv6 DNS name-servers " "can be configured" + ) + + if "client_ipv6_pool" in config: + ipv6_pool = config["client_ipv6_pool"] + if "delegate" in ipv6_pool: + if "prefix" not in ipv6_pool: + raise ConfigError( + 'IPv6 "delegate" also requires "prefix" to be defined!' + ) + + for delegate in ipv6_pool["delegate"]: + if "delegation_prefix" not in ipv6_pool["delegate"][delegate]: + raise ConfigError("delegation-prefix length required!") + + +def verify_accel_ppp_ip_pool(vpn_config): + """ + Common helper function which must be used by Accel-PPP + services (pptp, l2tp, sstp, pppoe) to verify client-ip-pool + """ + if dict_search("client_ip_pool", vpn_config): + for pool_name, pool_config in vpn_config["client_ip_pool"].items(): + next_pool = dict_search(f"next_pool", pool_config) + if next_pool: + if next_pool not in vpn_config["client_ip_pool"]: + raise ConfigError(f'Next pool "{next_pool}" does not exist') + if not dict_search(f"range", pool_config): + raise ConfigError( + f'Pool "{pool_name}" does not contain range but next-pool exists' + ) + + if not dict_search("gateway_address", vpn_config): + raise ConfigError("Server requires gateway-address to be configured!") + default_pool = dict_search("default_pool", vpn_config) + if default_pool: + if default_pool not in dict_search("client_ip_pool", vpn_config): + raise ConfigError(f'Default pool "{default_pool}" does not exists') diff --git a/python/vyos/configdiff.py b/python/vyos/configdiff.py index 1ec2dfafe..03b06c6d9 100644 --- a/python/vyos/configdiff.py +++ b/python/vyos/configdiff.py @@ -165,6 +165,30 @@ class ConfigDiff(object): return True return False + def node_changed_presence(self, path=[]) -> bool: + if self._diff_tree is None: + raise NotImplementedError("diff_tree class not available") + + path = self._make_path(path) + before = self._diff_tree.left.exists(path) + after = self._diff_tree.right.exists(path) + return (before and not after) or (not before and after) + + def node_changed_children(self, path=[]) -> list: + if self._diff_tree is None: + raise NotImplementedError("diff_tree class not available") + + path = self._make_path(path) + add = self._diff_tree.add + sub = self._diff_tree.sub + children = set() + if add.exists(path): + children.update(add.list_nodes(path)) + if sub.exists(path): + children.update(sub.list_nodes(path)) + + return list(children) + def get_child_nodes_diff_str(self, path=[]): ret = {'add': {}, 'change': {}, 'delete': {}} diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py index 9802ebae4..90842b749 100644 --- a/python/vyos/configsession.py +++ b/python/vyos/configsession.py @@ -30,8 +30,10 @@ SHOW_CONFIG = ['/bin/cli-shell-api', 'showConfig'] LOAD_CONFIG = ['/bin/cli-shell-api', 'loadFile'] MIGRATE_LOAD_CONFIG = ['/usr/libexec/vyos/vyos-load-config.py'] SAVE_CONFIG = ['/usr/libexec/vyos/vyos-save-config.py'] -INSTALL_IMAGE = ['/opt/vyatta/sbin/install-image', '--url'] -REMOVE_IMAGE = ['/opt/vyatta/bin/vyatta-boot-image.pl', '--del'] +INSTALL_IMAGE = ['/usr/libexec/vyos/op_mode/image_installer.py', + '--action', 'add', '--no-prompt', '--image-path'] +REMOVE_IMAGE = ['/usr/libexec/vyos/op_mode/image_manager.py', + '--action', 'delete', '--no-prompt', '--image-name'] GENERATE = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'generate'] SHOW = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'show'] RESET = ['/opt/vyatta/bin/vyatta-op-cmd-wrapper', 'reset'] diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 52f9238b8..27055c863 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -385,72 +385,6 @@ def verify_vlan_config(config): verify_mtu_parent(c_vlan, config) verify_mtu_parent(c_vlan, s_vlan) -def verify_accel_ppp_base_service(config, local_users=True): - """ - Common helper function which must be used by all Accel-PPP services based - on get_config_dict() - """ - # vertify auth settings - if local_users and dict_search('authentication.mode', config) == 'local': - if (dict_search(f'authentication.local_users', config) is None or - dict_search(f'authentication.local_users', config) == {}): - raise ConfigError( - 'Authentication mode local requires local users to be configured!') - - for user in dict_search('authentication.local_users.username', config): - user_config = config['authentication']['local_users']['username'][user] - - if 'password' not in user_config: - raise ConfigError(f'Password required for local user "{user}"') - - if 'rate_limit' in user_config: - # if up/download is set, check that both have a value - if not {'upload', 'download'} <= set(user_config['rate_limit']): - raise ConfigError(f'User "{user}" has rate-limit configured for only one ' \ - 'direction but both upload and download must be given!') - - elif dict_search('authentication.mode', config) == 'radius': - if not dict_search('authentication.radius.server', config): - raise ConfigError('RADIUS authentication requires at least one server') - - for server in dict_search('authentication.radius.server', config): - radius_config = config['authentication']['radius']['server'][server] - if 'key' not in radius_config: - raise ConfigError(f'Missing RADIUS secret key for server "{server}"') - - # Check global gateway or gateway in named pool - gateway = False - if 'gateway_address' in config: - gateway = True - else: - if 'client_ip_pool' in config: - if dict_search_recursive(config, 'gateway_address', ['client_ip_pool', 'name']): - for _, v in config['client_ip_pool']['name'].items(): - if 'gateway_address' in v: - gateway = True - break - if not gateway: - raise ConfigError('Server requires gateway-address to be configured!') - - if 'name_server_ipv4' in config: - if len(config['name_server_ipv4']) > 2: - raise ConfigError('Not more then two IPv4 DNS name-servers ' \ - 'can be configured') - - if 'name_server_ipv6' in config: - if len(config['name_server_ipv6']) > 3: - raise ConfigError('Not more then three IPv6 DNS name-servers ' \ - 'can be configured') - - if 'client_ipv6_pool' in config: - ipv6_pool = config['client_ipv6_pool'] - if 'delegate' in ipv6_pool: - if 'prefix' not in ipv6_pool: - raise ConfigError('IPv6 "delegate" also requires "prefix" to be defined!') - - for delegate in ipv6_pool['delegate']: - if 'delegation_prefix' not in ipv6_pool['delegate'][delegate]: - raise ConfigError('delegation-prefix length required!') def verify_diffie_hellman_length(file, min_keysize): """ Verify Diffie-Hellamn keypair length given via file. It must be greater diff --git a/python/vyos/remote.py b/python/vyos/remote.py index 8b90e4530..fec44b571 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -46,17 +46,6 @@ from vyos.version import get_version CHUNK_SIZE = 8192 -@contextmanager -def umask(mask: int): - """ - Context manager that temporarily sets the process umask. - """ - oldmask = os.umask(mask) - try: - yield - finally: - os.umask(oldmask) - class InteractivePolicy(MissingHostKeyPolicy): """ Paramiko policy for interactively querying the user on whether to proceed @@ -88,6 +77,17 @@ class SourceAdapter(HTTPAdapter): num_pools=connections, maxsize=maxsize, block=block, source_address=self._source_pair) +@contextmanager +def umask(mask: int): + """ + Context manager that temporarily sets the process umask. + """ + import os + oldmask = os.umask(mask) + try: + yield + finally: + os.umask(oldmask) def check_storage(path, size): """ @@ -436,8 +436,8 @@ def urlc(urlstring, *args, **kwargs): except KeyError: raise ValueError(f'Unsupported URL scheme: "{scheme}"') -def download(local_path, urlstring, progressbar=False, raise_error=False, check_space=False, - source_host='', source_port=0, timeout=10.0): +def download(local_path, urlstring, progressbar=False, check_space=False, + source_host='', source_port=0, timeout=10.0, raise_error=False): try: progressbar = progressbar and is_interactive() urlc(urlstring, progressbar, check_space, source_host, source_port, timeout).download(local_path) @@ -448,14 +448,12 @@ def download(local_path, urlstring, progressbar=False, raise_error=False, check_ except KeyboardInterrupt: print_error('\nDownload aborted by user.') -def upload(local_path, urlstring, progressbar=False, raise_error=False, +def upload(local_path, urlstring, progressbar=False, source_host='', source_port=0, timeout=10.0): try: progressbar = progressbar and is_interactive() urlc(urlstring, progressbar, source_host, source_port, timeout).upload(local_path) except Exception as err: - if raise_error: - raise print_error(f'Unable to upload "{urlstring}": {err}') except KeyboardInterrupt: print_error('\nUpload aborted by user.') diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py index 49e6b5c5e..f8e0fd1bf 100644 --- a/python/vyos/system/disk.py +++ b/python/vyos/system/disk.py @@ -150,7 +150,7 @@ def filesystem_create(partition: str, fstype: str) -> None: def partition_mount(partition: str, path: str, fsype: str = '', - overlay_params: dict[str, str] = {}) -> None: + overlay_params: dict[str, str] = {}) -> bool: """Mount a partition into a path Args: @@ -159,6 +159,9 @@ def partition_mount(partition: str, fsype (str): optionally, set fstype ('squashfs', 'overlay', 'iso9660') overlay_params (dict): optionally, set overlay parameters. Defaults to None. + + Returns: + bool: True on success """ if fsype in ['squashfs', 'iso9660']: command: str = f'mount -o loop,ro -t {fsype} {partition} {path}' @@ -171,7 +174,11 @@ def partition_mount(partition: str, else: command = f'mount {partition} {path}' - run(command) + rc = run(command) + if rc == 0: + return True + + return False def partition_umount(partition: str = '', path: str = '') -> None: diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py index 6c4e3bba5..c03ce02d5 100644 --- a/python/vyos/system/image.py +++ b/python/vyos/system/image.py @@ -261,3 +261,8 @@ def is_live_boot() -> bool: if boot_type == 'live': return True return False + +def is_running_as_container() -> bool: + if Path('/.dockerenv').exists(): + return True + return False diff --git a/python/vyos/template.py b/python/vyos/template.py index f79c7545e..2d4beeec2 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -664,8 +664,8 @@ def nat_static_rule(rule_conf, rule_id, nat_type): from vyos.nat import parse_nat_static_rule return parse_nat_static_rule(rule_conf, rule_id, nat_type) -@register_filter('conntrack_ignore_rule') -def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False): +@register_filter('conntrack_rule') +def conntrack_rule(rule_conf, rule_id, action, ipv6=False): ip_prefix = 'ip6' if ipv6 else 'ip' def_suffix = '6' if ipv6 else '' output = [] @@ -676,11 +676,15 @@ def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False): output.append(f'iifname {ifname}') if 'protocol' in rule_conf: - proto = rule_conf['protocol'] + if action != 'timeout': + proto = rule_conf['protocol'] + else: + for protocol, protocol_config in rule_conf['protocol'].items(): + proto = protocol output.append(f'meta l4proto {proto}') tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags') - if tcp_flags: + if tcp_flags and action != 'timeout': from vyos.firewall import parse_tcp_flags output.append(parse_tcp_flags(tcp_flags)) @@ -743,11 +747,24 @@ def conntrack_ignore_rule(rule_conf, rule_id, ipv6=False): output.append(f'{proto} {prefix}port {operator} @P_{group_name}') - output.append('counter notrack') - output.append(f'comment "ignore-{rule_id}"') + if action == 'ignore': + output.append('counter notrack') + output.append(f'comment "ignore-{rule_id}"') + else: + output.append(f'counter ct timeout set ct-timeout-{rule_id}') + output.append(f'comment "timeout-{rule_id}"') return " ".join(output) +@register_filter('conntrack_ct_policy') +def conntrack_ct_policy(protocol_conf): + output = [] + for item in protocol_conf: + item_value = protocol_conf[item] + output.append(f'{item}: {item_value}') + + return ", ".join(output) + @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 6a5de5423..2a0808fca 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -61,14 +61,17 @@ def get_vrf_members(vrf: str) -> list: """ import json from vyos.utils.process import cmd - if not interface_exists(vrf): - raise ValueError(f'VRF "{vrf}" does not exist!') - output = cmd(f'ip --json --brief link show master {vrf}') - answer = json.loads(output) interfaces = [] - for data in answer: - if 'ifname' in data: - interfaces.append(data.get('ifname')) + try: + if not interface_exists(vrf): + raise ValueError(f'VRF "{vrf}" does not exist!') + output = cmd(f'ip --json --brief link show vrf {vrf}') + answer = json.loads(output) + for data in answer: + if 'ifname' in data: + interfaces.append(data.get('ifname')) + except: + pass return interfaces def get_interface_vrf(interface): diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index 989028f64..32624719f 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -11,10 +11,10 @@ # # 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 base_vyostest_shim import VyOSUnitTestSHIM from configparser import ConfigParser @@ -25,12 +25,12 @@ from vyos.utils.system import get_half_cpus from vyos.utils.process import process_named_running from vyos.utils.process import cmd + class BasicAccelPPPTest: class TestCase(VyOSUnitTestSHIM.TestCase): - @classmethod def setUpClass(cls): - cls._process_name = 'accel-pppd' + cls._process_name = "accel-pppd" super(BasicAccelPPPTest.TestCase, cls).setUpClass() @@ -39,7 +39,7 @@ class BasicAccelPPPTest: cls.cli_delete(cls, cls._base_path) def setUp(self): - self._gateway = '192.0.2.1' + self._gateway = "192.0.2.1" # ensure we can also run this test on a live system - so lets clean # out the current configuration :) self.cli_delete(self._base_path) @@ -60,84 +60,189 @@ class BasicAccelPPPTest: def delete(self, path): self.cli_delete(self._base_path + path) - def basic_config(self): - # PPPoE local auth mode requires local users to be configured! - self.set(['authentication', 'local-users', 'username', 'vyos', 'password', 'vyos']) - self.set(['authentication', 'mode', 'local']) - self.set(['gateway-address', self._gateway]) + def basic_protocol_specific_config(self): + """ + An astract method. + Initialize protocol scpecific configureations. + """ + self.assertFalse(True, msg="Function must be defined") + + def initial_auth_config(self): + """ + Initialization of default authentication for all protocols + """ + self.set( + [ + "authentication", + "local-users", + "username", + "vyos", + "password", + "vyos", + ] + ) + self.set(["authentication", "mode", "local"]) + + def initial_gateway_config(self): + """ + Initialization of default gateway + """ + self.set(["gateway-address", self._gateway]) + + def initial_pool_config(self): + """ + Initialization of default client ip pool + """ + first_pool = "SIMPLE-POOL" + self.set(["client-ip-pool", first_pool, "range", "192.0.2.0/24"]) + self.set(["default-pool", first_pool]) + + def basic_config(self, is_auth=True, is_gateway=True, is_client_pool=True): + """ + Initialization of basic configuration + :param is_auth: authentication initialization + :type is_auth: bool + :param is_gateway: gateway initialization + :type is_gateway: bool + :param is_client_pool: client ip pool initialization + :type is_client_pool: bool + """ + self.basic_protocol_specific_config() + if is_auth: + self.initial_auth_config() + if is_gateway: + self.initial_gateway_config() + if is_client_pool: + self.initial_pool_config() + + def getConfig(self, start, end="cli"): + """ + Return part of configuration from line + where the first injection of start keyword to the line + where the first injection of end keyowrd + :param start: start keyword + :type start: str + :param end: end keyword + :type end: str + :return: part of config + :rtype: str + """ + command = f'cat {self._config_file} | sed -n "/^\[{start}/,/^\[{end}/p"' + out = cmd(command) + return out def verify(self, conf): - self.assertEqual(conf['core']['thread-count'], str(get_half_cpus())) + self.assertEqual(conf["core"]["thread-count"], str(get_half_cpus())) def test_accel_name_servers(self): # Verify proper Name-Server configuration for IPv4 and IPv6 self.basic_config() - nameserver = ['192.0.2.1', '192.0.2.2', '2001:db8::1'] + nameserver = ["192.0.2.1", "192.0.2.2", "2001:db8::1"] for ns in nameserver: - self.set(['name-server', ns]) + self.set(["name-server", ns]) # commit changes self.cli_commit() # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) conf.read(self._config_file) # IPv4 and IPv6 nameservers must be checked individually for ns in nameserver: if is_ipv4(ns): - self.assertIn(ns, [conf['dns']['dns1'], conf['dns']['dns2']]) + self.assertIn(ns, [conf["dns"]["dns1"], conf["dns"]["dns2"]]) else: - self.assertEqual(conf['ipv6-dns'][ns], None) + self.assertEqual(conf["ipv6-dns"][ns], None) def test_accel_local_authentication(self): # Test configuration of local authentication self.basic_config() # upload / download limit - user = 'test' - password = 'test2' - static_ip = '100.100.100.101' - upload = '5000' - download = '10000' - - self.set(['authentication', 'local-users', 'username', user, 'password', password]) - self.set(['authentication', 'local-users', 'username', user, 'static-ip', static_ip]) - self.set(['authentication', 'local-users', 'username', user, 'rate-limit', 'upload', upload]) + user = "test" + password = "test2" + static_ip = "100.100.100.101" + upload = "5000" + download = "10000" + + self.set( + [ + "authentication", + "local-users", + "username", + user, + "password", + password, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "static-ip", + static_ip, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "rate-limit", + "upload", + upload, + ] + ) # upload rate-limit requires also download rate-limit with self.assertRaises(ConfigSessionError): self.cli_commit() - self.set(['authentication', 'local-users', 'username', user, 'rate-limit', 'download', download]) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "rate-limit", + "download", + download, + ] + ) # commit changes self.cli_commit() # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) conf.read(self._config_file) # check proper path to chap-secrets file - self.assertEqual(conf['chap-secrets']['chap-secrets'], self._chap_secrets) + self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets) # basic verification self.verify(conf) # check local users - tmp = cmd(f'sudo cat {self._chap_secrets}') - regex = f'{user}\s+\*\s+{password}\s+{static_ip}\s+{download}/{upload}' + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+{static_ip}\s+{download}/{upload}" tmp = re.findall(regex, tmp) self.assertTrue(tmp) # Check local-users default value(s) - self.delete(['authentication', 'local-users', 'username', user, 'static-ip']) + self.delete( + ["authentication", "local-users", "username", user, "static-ip"] + ) # commit changes self.cli_commit() # check local users - tmp = cmd(f'sudo cat {self._chap_secrets}') - regex = f'{user}\s+\*\s+{password}\s+\*\s+{download}/{upload}' + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+\*\s+{download}/{upload}" tmp = re.findall(regex, tmp) self.assertTrue(tmp) @@ -145,74 +250,170 @@ class BasicAccelPPPTest: # Test configuration of RADIUS authentication for PPPoE server self.basic_config() - radius_server = '192.0.2.22' - radius_key = 'secretVyOS' - radius_port = '2000' - radius_port_acc = '3000' - - self.set(['authentication', 'mode', 'radius']) - self.set(['authentication', 'radius', 'server', radius_server, 'key', radius_key]) - self.set(['authentication', 'radius', 'server', radius_server, 'port', radius_port]) - self.set(['authentication', 'radius', 'server', radius_server, 'acct-port', radius_port_acc]) - - coa_server = '4.4.4.4' - coa_key = 'testCoA' - self.set(['authentication', 'radius', 'dynamic-author', 'server', coa_server]) - self.set(['authentication', 'radius', 'dynamic-author', 'key', coa_key]) - - nas_id = 'VyOS-PPPoE' - nas_ip = '7.7.7.7' - self.set(['authentication', 'radius', 'nas-identifier', nas_id]) - self.set(['authentication', 'radius', 'nas-ip-address', nas_ip]) - - source_address = '1.2.3.4' - self.set(['authentication', 'radius', 'source-address', source_address]) + radius_server = "192.0.2.22" + radius_key = "secretVyOS" + radius_port = "2000" + radius_port_acc = "3000" + + self.set(["authentication", "mode", "radius"]) + self.set( + ["authentication", "radius", "server", radius_server, "key", radius_key] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "port", + radius_port, + ] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "acct-port", + radius_port_acc, + ] + ) + + coa_server = "4.4.4.4" + coa_key = "testCoA" + self.set( + ["authentication", "radius", "dynamic-author", "server", coa_server] + ) + self.set(["authentication", "radius", "dynamic-author", "key", coa_key]) + + nas_id = "VyOS-PPPoE" + nas_ip = "7.7.7.7" + self.set(["authentication", "radius", "nas-identifier", nas_id]) + self.set(["authentication", "radius", "nas-ip-address", nas_ip]) + + source_address = "1.2.3.4" + self.set(["authentication", "radius", "source-address", source_address]) # commit changes self.cli_commit() # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) conf.read(self._config_file) # basic verification self.verify(conf) # check auth - self.assertTrue(conf['radius'].getboolean('verbose')) - self.assertEqual(conf['radius']['acct-timeout'], '3') - self.assertEqual(conf['radius']['timeout'], '3') - self.assertEqual(conf['radius']['max-try'], '3') - - self.assertEqual(conf['radius']['dae-server'], f'{coa_server}:1700,{coa_key}') - self.assertEqual(conf['radius']['nas-identifier'], nas_id) - self.assertEqual(conf['radius']['nas-ip-address'], nas_ip) - self.assertEqual(conf['radius']['bind'], source_address) - - server = conf['radius']['server'].split(',') + self.assertTrue(conf["radius"].getboolean("verbose")) + self.assertEqual(conf["radius"]["acct-timeout"], "3") + self.assertEqual(conf["radius"]["timeout"], "3") + self.assertEqual(conf["radius"]["max-try"], "3") + + self.assertEqual( + conf["radius"]["dae-server"], f"{coa_server}:1700,{coa_key}" + ) + self.assertEqual(conf["radius"]["nas-identifier"], nas_id) + self.assertEqual(conf["radius"]["nas-ip-address"], nas_ip) + self.assertEqual(conf["radius"]["bind"], source_address) + + server = conf["radius"]["server"].split(",") self.assertEqual(radius_server, server[0]) self.assertEqual(radius_key, server[1]) - self.assertEqual(f'auth-port={radius_port}', server[2]) - self.assertEqual(f'acct-port={radius_port_acc}', server[3]) - self.assertEqual(f'req-limit=0', server[4]) - self.assertEqual(f'fail-time=0', server[5]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"acct-port={radius_port_acc}", server[3]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) # # Disable Radius Accounting # - self.delete(['authentication', 'radius', 'server', radius_server, 'acct-port']) - self.set(['authentication', 'radius', 'server', radius_server, 'disable-accounting']) + self.delete( + ["authentication", "radius", "server", radius_server, "acct-port"] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "disable-accounting", + ] + ) # commit changes self.cli_commit() conf.read(self._config_file) - server = conf['radius']['server'].split(',') + server = conf["radius"]["server"].split(",") self.assertEqual(radius_server, server[0]) self.assertEqual(radius_key, server[1]) - self.assertEqual(f'auth-port={radius_port}', server[2]) - self.assertEqual(f'acct-port=0', server[3]) - self.assertEqual(f'req-limit=0', server[4]) - self.assertEqual(f'fail-time=0', server[5]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"acct-port=0", server[3]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) + + def test_accel_ipv4_pool(self): + """ + Test accel-ppp IPv4 pool + """ + self.basic_config(is_gateway=False, is_client_pool=False) + gateway = "192.0.2.1" + subnet = "172.16.0.0/24" + first_pool = "POOL1" + second_pool = "POOL2" + range = "192.0.2.10-192.0.2.20" + + self.set(["gateway-address", gateway]) + self.set(["client-ip-pool", first_pool, "range", subnet]) + self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) + self.set(["client-ip-pool", second_pool, "range", range]) + self.set(["default-pool", first_pool]) + # commit changes + + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + self.assertEqual( + f"{first_pool},next={second_pool}", conf["ip-pool"][f"{subnet},name"] + ) + self.assertEqual(second_pool, conf["ip-pool"][f"{range},name"]) + self.assertEqual(gateway, conf["ip-pool"]["gw-ip-address"]) + self.assertEqual(first_pool, conf[self._protocol_section]["ip-pool"]) + + def test_accel_next_pool(self): + """ + T5099 required specific order + """ + self.basic_config(is_gateway=False, is_client_pool=False) + + gateway = "192.0.2.1" + first_pool = "VyOS-pool1" + first_subnet = "192.0.2.0/25" + second_pool = "Vyos-pool2" + second_subnet = "203.0.113.0/25" + third_pool = "Vyos-pool3" + third_subnet = "198.51.100.0/24" + + self.set(["gateway-address", gateway]) + self.set(["client-ip-pool", first_pool, "range", first_subnet]) + self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) + self.set(["client-ip-pool", second_pool, "range", second_subnet]) + self.set(["client-ip-pool", second_pool, "next-pool", third_pool]) + self.set(["client-ip-pool", third_pool, "range", third_subnet]) + + # commit changes + self.cli_commit() + + config = self.getConfig("ip-pool") + pool_config = f"""gw-ip-address={gateway} +{third_subnet},name={third_pool} +{second_subnet},name={second_pool},next={third_pool} +{first_subnet},name={first_pool},next={second_pool}""" + self.assertIn(pool_config, config) diff --git a/smoketest/scripts/cli/test_nat64.py b/smoketest/scripts/cli/test_nat64.py new file mode 100755 index 000000000..b5723ac7e --- /dev/null +++ b/smoketest/scripts/cli/test_nat64.py @@ -0,0 +1,102 @@ +#!/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/>. + +import json +import os +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configsession import ConfigSessionError +from vyos.utils.process import cmd +from vyos.utils.dict import dict_search + +base_path = ['nat64'] +src_path = base_path + ['source'] + +jool_nat64_config = '/run/jool/instance-100.json' + + +class TestNAT64(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestNAT64, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + def tearDown(self): + self.cli_delete(base_path) + self.cli_commit() + self.assertFalse(os.path.exists(jool_nat64_config)) + + def test_snat64(self): + rule = '100' + translation_rule = '10' + prefix_v6 = '64:ff9b::/96' + pool = '192.0.2.10' + pool_port = '1-65535' + + self.cli_set(src_path + ['rule', rule, 'source', 'prefix', prefix_v6]) + self.cli_set( + src_path + + ['rule', rule, 'translation', 'pool', translation_rule, 'address', pool] + ) + self.cli_set( + src_path + + ['rule', rule, 'translation', 'pool', translation_rule, 'port', pool_port] + ) + self.cli_commit() + + # Load the JSON file + with open(f'/run/jool/instance-{rule}.json', 'r') as json_file: + config_data = json.load(json_file) + + # Assertions based on the content of the JSON file + self.assertEqual(config_data['instance'], f'instance-{rule}') + self.assertEqual(config_data['framework'], 'netfilter') + self.assertEqual(config_data['global']['pool6'], prefix_v6) + self.assertTrue(config_data['global']['manually-enabled']) + + # Check the pool4 entries + pool4_entries = config_data.get('pool4', []) + self.assertIsInstance(pool4_entries, list) + self.assertGreater(len(pool4_entries), 0) + + for entry in pool4_entries: + self.assertIn('protocol', entry) + self.assertIn('prefix', entry) + self.assertIn('port range', entry) + + protocol = entry['protocol'] + prefix = entry['prefix'] + port_range = entry['port range'] + + if protocol == 'ICMP': + self.assertEqual(prefix, pool) + self.assertEqual(port_range, pool_port) + elif protocol == 'UDP': + self.assertEqual(prefix, pool) + self.assertEqual(port_range, pool_port) + elif protocol == 'TCP': + self.assertEqual(prefix, pool) + self.assertEqual(port_range, pool_port) + else: + self.fail(f'Unexpected protocol: {protocol}') + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 51a33f978..c21d8af4e 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1107,6 +1107,33 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): 'metric' : '-20', }, }, + '30': { + 'action': 'permit', + 'match': { + 'ip-nexthop-addr': ipv4_nexthop_address, + }, + 'set': { + 'metric': 'rtt', + }, + }, + '40': { + 'action': 'permit', + 'match': { + 'ip-nexthop-addr': ipv4_nexthop_address, + }, + 'set': { + 'metric': '+rtt', + }, + }, + '50': { + 'action': 'permit', + 'match': { + 'ip-nexthop-addr': ipv4_nexthop_address, + }, + 'set': { + 'metric': '-rtt', + }, + }, }, }, } diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index 451565664..f209eae3a 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -77,11 +77,23 @@ profiles = { } class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestProtocolsBFD, cls).setUpClass() + + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_bfd_peer(self): self.cli_set(['vrf', 'name', vrf_name, 'table', '1000']) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 23e138ebe..71e2142f9 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -174,9 +174,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): def setUpClass(cls): super(TestProtocolsBGP, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['policy', 'route-map']) + cls.cli_delete(cls, ['policy', 'prefix-list']) + cls.cli_delete(cls, ['policy', 'prefix-list6']) + cls.cli_delete(cls, ['vrf']) cls.cli_set(cls, ['policy', 'route-map', route_map_in, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map_out, 'rule', '10', 'action', 'permit']) @@ -192,18 +199,23 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): @classmethod def tearDownClass(cls): - cls.cli_delete(cls, ['policy']) + cls.cli_delete(cls, ['policy', 'route-map']) + cls.cli_delete(cls, ['policy', 'prefix-list']) + cls.cli_delete(cls, ['policy', 'prefix-list6']) def setUp(self): self.cli_set(base_path + ['system-as', ASN]) def tearDown(self): + # cleanup any possible VRF mess self.cli_delete(['vrf']) + # always destrox the entire bgpd configuration to make the processes + # life as hard as possible self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def create_bgp_instances_for_import_test(self): table = '1000' diff --git a/smoketest/scripts/cli/test_protocols_igmp-proxy.py b/smoketest/scripts/cli/test_protocols_igmp-proxy.py index a75003b12..df10442ea 100755 --- a/smoketest/scripts/cli/test_protocols_igmp-proxy.py +++ b/smoketest/scripts/cli/test_protocols_igmp-proxy.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 @@ -29,14 +29,32 @@ upstream_if = 'eth1' downstream_if = 'eth2' class TestProtocolsIGMPProxy(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['interfaces', 'ethernet', upstream_if, 'address', '172.16.1.1/24']) + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsIGMPProxy, cls).setUpClass() + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + cls.cli_set(cls, ['interfaces', 'ethernet', upstream_if, 'address', '172.16.1.1/24']) + + @classmethod + def tearDownClass(cls): + cls.cli_delete(cls, ['interfaces', 'ethernet', upstream_if, 'address']) + + # call base-classes classmethod + super(TestProtocolsIGMPProxy, cls).tearDownClass() def tearDown(self): - self.cli_delete(['interfaces', 'ethernet', upstream_if, 'address']) + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + self.cli_delete(base_path) self.cli_commit() + # Check for no longer running process + self.assertFalse(process_named_running(PROCESS_NAME)) + def test_igmpproxy(self): threshold = '20' altnet = '192.0.2.0/24' @@ -74,8 +92,5 @@ class TestProtocolsIGMPProxy(VyOSUnitTestSHIM.TestCase): self.assertIn(f'whitelist {whitelist}', config) self.assertIn(f'phyint {downstream_if} downstream ratelimit 0 threshold 1', config) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) - if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 8b423dbea..aa5f2f38c 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-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,20 +31,25 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): cls._interfaces = Section.interfaces('ethernet') - # call base-classes classmethod super(TestProtocolsISIS, cls).setUpClass() - + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['vrf']) def tearDown(self): + # cleanup any possible VRF mess + self.cli_delete(['vrf']) + # always destrox the entire isisd configuration to make the processes + # life as hard as possible self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def isis_base_config(self): self.cli_set(base_path + ['net', net]) @@ -333,7 +338,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['interface', interface]) self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'action', 'permit']) self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', '1', 'prefix', prefix_list_address]) - + # Commit main ISIS changes self.cli_commit() @@ -385,4 +390,4 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() if __name__ == '__main__': - unittest.main(verbosity=2)
\ No newline at end of file + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py index 06f21c6e1..0c1599f9b 100755 --- a/smoketest/scripts/cli/test_protocols_mpls.py +++ b/smoketest/scripts/cli/test_protocols_mpls.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -70,6 +70,9 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): def setUpClass(cls): super(TestProtocolsMPLS, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) @@ -77,8 +80,9 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase): def tearDown(self): self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_mpls_basic(self): router_id = '1.2.3.4' diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index a6850db71..6bffc7c45 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-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,6 +32,9 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def setUpClass(cls): super(TestProtocolsOSPF, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) @@ -45,11 +48,12 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): super(TestProtocolsOSPF, cls).tearDownClass() def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_commit() + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + def test_ospf_01_defaults(self): # commit changes self.cli_set(base_path) diff --git a/smoketest/scripts/cli/test_protocols_ospfv3.py b/smoketest/scripts/cli/test_protocols_ospfv3.py index 0d6c6c691..4ae7f05d9 100755 --- a/smoketest/scripts/cli/test_protocols_ospfv3.py +++ b/smoketest/scripts/cli/test_protocols_ospfv3.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-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,9 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): def setUpClass(cls): super(TestProtocolsOSPFv3, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '20', 'action', 'permit']) @@ -48,11 +51,12 @@ class TestProtocolsOSPFv3(VyOSUnitTestSHIM.TestCase): super(TestProtocolsOSPFv3, cls).tearDownClass() def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) self.cli_commit() + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) + def test_ospfv3_01_basic(self): seq = '10' prefix = '2001:db8::/32' diff --git a/smoketest/scripts/cli/test_protocols_pim6.py b/smoketest/scripts/cli/test_protocols_pim6.py index e22a7c722..ba24edca2 100755 --- a/smoketest/scripts/cli/test_protocols_pim6.py +++ b/smoketest/scripts/cli/test_protocols_pim6.py @@ -25,15 +25,22 @@ PROCESS_NAME = 'pim6d' base_path = ['protocols', 'pim6'] class TestProtocolsPIMv6(VyOSUnitTestSHIM.TestCase): - def tearDown(self): - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsPIMv6, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_pim6_01_mld_simple(self): # commit changes diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 925499fc8..bfc327fd4 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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,8 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestProtocolsRIP, cls).setUpClass() - + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) @@ -65,8 +66,8 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_rip_01_parameters(self): distance = '40' diff --git a/smoketest/scripts/cli/test_protocols_ripng.py b/smoketest/scripts/cli/test_protocols_ripng.py index 0a8ce7eef..0cfb065c6 100755 --- a/smoketest/scripts/cli/test_protocols_ripng.py +++ b/smoketest/scripts/cli/test_protocols_ripng.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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,28 +31,43 @@ route_map = 'FooBar123' base_path = ['protocols', 'ripng'] class TestProtocolsRIPng(VyOSUnitTestSHIM.TestCase): - def setUp(self): - self.cli_set(['policy', 'access-list6', acl_in, 'rule', '10', 'action', 'permit']) - self.cli_set(['policy', 'access-list6', acl_in, 'rule', '10', 'source', 'any']) - self.cli_set(['policy', 'access-list6', acl_out, 'rule', '20', 'action', 'deny']) - self.cli_set(['policy', 'access-list6', acl_out, 'rule', '20', 'source', 'any']) - self.cli_set(['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'action', 'permit']) - self.cli_set(['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'prefix', '2001:db8::/32']) - self.cli_set(['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'action', 'deny']) - self.cli_set(['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'prefix', '2001:db8::/32']) - self.cli_set(['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsRIPng, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + + cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'access-list6', acl_in, 'rule', '10', 'source', 'any']) + cls.cli_set(cls, ['policy', 'access-list6', acl_out, 'rule', '20', 'action', 'deny']) + cls.cli_set(cls, ['policy', 'access-list6', acl_out, 'rule', '20', 'source', 'any']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'action', 'permit']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_in, 'rule', '100', 'prefix', '2001:db8::/32']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'action', 'deny']) + cls.cli_set(cls, ['policy', 'prefix-list6', prefix_list_out, 'rule', '200', 'prefix', '2001:db8::/32']) + cls.cli_set(cls, ['policy', 'route-map', route_map, 'rule', '10', 'action', 'permit']) + + @classmethod + def tearDownClass(cls): + # call base-classes classmethod + super(TestProtocolsRIPng, cls).tearDownClass() + + cls.cli_delete(cls, ['policy', 'access-list6', acl_in]) + cls.cli_delete(cls, ['policy', 'access-list6', acl_out]) + cls.cli_delete(cls, ['policy', 'prefix-list6', prefix_list_in]) + cls.cli_delete(cls, ['policy', 'prefix-list6', prefix_list_out]) + cls.cli_delete(cls, ['policy', 'route-map', route_map]) def tearDown(self): self.cli_delete(base_path) - self.cli_delete(['policy', 'access-list6', acl_in]) - self.cli_delete(['policy', 'access-list6', acl_out]) - self.cli_delete(['policy', 'prefix-list6', prefix_list_in]) - self.cli_delete(['policy', 'prefix-list6', prefix_list_out]) - self.cli_delete(['policy', 'route-map', route_map]) self.cli_commit() - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_ripng_01_parameters(self): metric = '8' diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index f4aedcbc3..ab3f076ac 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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,16 @@ rpki_ssh_key = '/config/auth/id_rsa_rpki' rpki_ssh_pub = f'{rpki_ssh_key}.pub' class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + # call base-classes classmethod + super(TestProtocolsRPKI, cls).setUpClass() + # Retrieve FRR daemon PID - it is not allowed to crash, thus PID must remain the same + cls.daemon_pid = process_named_running(PROCESS_NAME) + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() @@ -39,8 +49,8 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): # frrconfig = self.getFRRconfig('rpki') # self.assertNotIn('rpki', frrconfig) - # Check for running process - self.assertTrue(process_named_running(PROCESS_NAME)) + # check process health and continuity + self.assertEqual(self.daemon_pid, process_named_running(PROCESS_NAME)) def test_rpki(self): polling = '7200' diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index fe213a8ae..cb3d90593 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -32,6 +32,7 @@ DDCLIENT_PID = '/run/ddclient/ddclient.pid' DDCLIENT_PNAME = 'ddclient' base_path = ['service', 'dns', 'dynamic'] +name_path = base_path + ['name'] server = 'ddns.vyos.io' hostname = 'test.ddns.vyos.io' zone = 'vyos.io' @@ -58,38 +59,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv4 standard DDNS service configuration def test_01_dyndns_service_standard(self): - svc_path = ['address', interface, 'service'] services = {'cloudflare': {'protocol': 'cloudflare'}, 'freedns': {'protocol': 'freedns', 'username': username}, 'zoneedit': {'protocol': 'zoneedit1', 'username': username}} for svc, details in services.items(): - self.cli_set(base_path + svc_path + [svc, 'host-name', hostname]) - self.cli_set(base_path + svc_path + [svc, 'password', password]) - self.cli_set(base_path + svc_path + [svc, 'zone', zone]) - self.cli_set(base_path + svc_path + [svc, 'ttl', ttl]) + self.cli_set(name_path + [svc, 'address', interface]) + self.cli_set(name_path + [svc, 'host-name', hostname]) + self.cli_set(name_path + [svc, 'password', password]) + self.cli_set(name_path + [svc, 'zone', zone]) + self.cli_set(name_path + [svc, 'ttl', ttl]) for opt, value in details.items(): - self.cli_set(base_path + svc_path + [svc, opt, value]) + self.cli_set(name_path + [svc, opt, value]) # 'zone' option is supported and required by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + svc_path + [svc, 'zone', zone]) + self.cli_set(name_path + [svc, 'zone', zone]) if details['protocol'] == 'cloudflare': pass else: # exception is raised for unsupported ones with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + svc_path + [svc, 'zone']) + self.cli_delete(name_path + [svc, 'zone']) # 'ttl' option is supported by 'cloudfare', but not 'freedns' and 'zoneedit' - self.cli_set(base_path + svc_path + [svc, 'ttl', ttl]) + self.cli_set(name_path + [svc, 'ttl', ttl]) if details['protocol'] == 'cloudflare': pass else: # exception is raised for unsupported ones with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + svc_path + [svc, 'ttl']) + self.cli_delete(name_path + [svc, 'ttl']) # commit changes self.cli_commit() @@ -113,7 +114,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv6 only DDNS service configuration def test_02_dyndns_service_ipv6(self): interval = '60' - svc_path = ['address', interface, 'service', 'dynv6'] + svc_path = name_path + ['dynv6'] proto = 'dyndns2' ip_version = 'ipv6' wait_time = '600' @@ -121,19 +122,20 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_bad = '360' self.cli_set(base_path + ['interval', interval]) - self.cli_set(base_path + svc_path + ['ip-version', ip_version]) - self.cli_set(base_path + svc_path + ['protocol', proto]) - self.cli_set(base_path + svc_path + ['server', server]) - self.cli_set(base_path + svc_path + ['username', username]) - self.cli_set(base_path + svc_path + ['password', password]) - self.cli_set(base_path + svc_path + ['host-name', hostname]) - self.cli_set(base_path + svc_path + ['wait-time', wait_time]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['ip-version', ip_version]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['username', username]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['wait-time', wait_time]) # expiry-time must be greater than wait-time, exception is raised otherwise - self.cli_set(base_path + svc_path + ['expiry-time', expiry_time_bad]) with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['expiry-time', expiry_time_bad]) self.cli_commit() - self.cli_set(base_path + svc_path + ['expiry-time', expiry_time_good]) + self.cli_set(svc_path + ['expiry-time', expiry_time_good]) # commit changes self.cli_commit() @@ -152,25 +154,25 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # IPv4+IPv6 dual DDNS service configuration def test_03_dyndns_service_dual_stack(self): - svc_path = ['address', interface, 'service'] services = {'cloudflare': {'protocol': 'cloudflare', 'zone': zone}, 'freedns': {'protocol': 'freedns', 'username': username}, 'google': {'protocol': 'googledomains', 'username': username}} ip_version = 'both' for name, details in services.items(): - self.cli_set(base_path + svc_path + [name, 'host-name', hostname]) - self.cli_set(base_path + svc_path + [name, 'password', password]) + self.cli_set(name_path + [name, 'address', interface]) + self.cli_set(name_path + [name, 'host-name', hostname]) + self.cli_set(name_path + [name, 'password', password]) for opt, value in details.items(): - self.cli_set(base_path + svc_path + [name, opt, value]) + self.cli_set(name_path + [name, opt, value]) # Dual stack is supported by 'cloudfare' and 'freedns' but not 'googledomains' # exception is raised for unsupported ones - self.cli_set(base_path + svc_path + [name, 'ip-version', ip_version]) + self.cli_set(name_path + [name, 'ip-version', ip_version]) if details['protocol'] not in ['cloudflare', 'freedns']: with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_delete(base_path + svc_path + [name, 'ip-version']) + self.cli_delete(name_path + [name, 'ip-version']) # commit changes self.cli_commit() @@ -197,16 +199,19 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_04_dyndns_rfc2136(self): # Check if DDNS service can be configured and runs - svc_path = ['address', interface, 'rfc2136', 'vyos'] + svc_path = name_path + ['vyos'] + proto = 'nsupdate' with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: key_file.write(b'S3cretKey') - self.cli_set(base_path + svc_path + ['server', server]) - self.cli_set(base_path + svc_path + ['zone', zone]) - self.cli_set(base_path + svc_path + ['key', key_file.name]) - self.cli_set(base_path + svc_path + ['ttl', ttl]) - self.cli_set(base_path + svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['key', key_file.name]) + self.cli_set(svc_path + ['ttl', ttl]) + self.cli_set(svc_path + ['host-name', hostname]) # commit changes self.cli_commit() @@ -215,7 +220,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') self.assertIn(f'use=if', ddclient_conf) self.assertIn(f'if={interface}', ddclient_conf) - self.assertIn(f'protocol=nsupdate', ddclient_conf) + self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'server={server}', ddclient_conf) self.assertIn(f'zone={zone}', ddclient_conf) self.assertIn(f'password=\'{key_file.name}\'', ddclient_conf) @@ -223,16 +228,17 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_05_dyndns_hostname(self): # Check if DDNS service can be configured and runs - svc_path = ['address', interface, 'service', 'namecheap'] + svc_path = name_path + ['namecheap'] proto = 'namecheap' hostnames = ['@', 'www', hostname, f'@.{hostname}'] for name in hostnames: - self.cli_set(base_path + svc_path + ['protocol', proto]) - self.cli_set(base_path + svc_path + ['server', server]) - self.cli_set(base_path + svc_path + ['username', username]) - self.cli_set(base_path + svc_path + ['password', password]) - self.cli_set(base_path + svc_path + ['host-name', name]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['server', server]) + self.cli_set(svc_path + ['username', username]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', name]) # commit changes self.cli_commit() @@ -247,42 +253,32 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): def test_06_dyndns_web_options(self): # Check if DDNS service can be configured and runs - base_path_iface = base_path + ['address', interface] - base_path_web = base_path + ['address', 'web'] - svc_path_iface = base_path_iface + ['service', 'cloudflare'] - svc_path_web = base_path_web + ['service', 'cloudflare'] + svc_path = name_path + ['cloudflare'] proto = 'cloudflare' web_url_good = 'https://ifconfig.me/ip' web_url_bad = 'http:/ifconfig.me/ip' - self.cli_set(svc_path_iface + ['protocol', proto]) - self.cli_set(svc_path_iface + ['zone', zone]) - self.cli_set(svc_path_iface + ['password', password]) - self.cli_set(svc_path_iface + ['host-name', hostname]) - self.cli_set(base_path_iface + ['web-options', 'url', web_url_good]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['password', password]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['web-options', 'url', web_url_good]) # web-options is supported only with web service based address lookup # exception is raised for interface based address lookup with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['address', interface]) self.cli_commit() - self.cli_delete(base_path_iface + ['web-options']) + self.cli_set(svc_path + ['address', 'web']) # commit changes self.cli_commit() - # web-options is supported with web service based address lookup - # this should work, but clear interface based config first - self.cli_delete(base_path_iface) - self.cli_set(svc_path_web + ['protocol', proto]) - self.cli_set(svc_path_web + ['zone', zone]) - self.cli_set(svc_path_web + ['password', password]) - self.cli_set(svc_path_web + ['host-name', hostname]) - # web-options must be a valid URL - with self.assertRaises(ConfigSessionError) as cm: - self.cli_set(base_path_web + ['web-options', 'url', web_url_bad]) - self.assertIn(f'"{web_url_bad.removeprefix("http:")}" is not a valid URI', str(cm.exception)) - self.cli_set(base_path_web + ['web-options', 'url', web_url_good]) + with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['web-options', 'url', web_url_bad]) + self.cli_commit() + self.cli_set(svc_path + ['web-options', 'url', web_url_good]) # commit changes self.cli_commit() @@ -300,15 +296,17 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Table number randomized, but should be within range 100-65535 vrf_table = "".join(random.choices(string.digits, k=4)) vrf_name = f'vyos-test-{vrf_table}' - svc_path = ['address', interface, 'service', 'cloudflare'] + svc_path = name_path + ['cloudflare'] + proto = 'cloudflare' self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table]) self.cli_set(base_path + ['vrf', vrf_name]) - self.cli_set(base_path + svc_path + ['protocol', 'cloudflare']) - self.cli_set(base_path + svc_path + ['host-name', hostname]) - self.cli_set(base_path + svc_path + ['zone', zone]) - self.cli_set(base_path + svc_path + ['password', password]) + self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['protocol', proto]) + self.cli_set(svc_path + ['host-name', hostname]) + self.cli_set(svc_path + ['zone', zone]) + self.cli_set(svc_path + ['password', password]) # commit changes self.cli_commit() diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 24e1f1299..6cb91bcf1 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -254,6 +254,35 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.assertTrue(success) @ignore_warning(InsecureRequestWarning) + def test_api_add_delete(self): + address = '127.0.0.1' + key = 'VyOS-key' + url = f'https://{address}/retrieve' + payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'} + headers = {} + + self.cli_set(base_path) + self.cli_commit() + + r = request('POST', url, verify=False, headers=headers, data=payload) + # api not configured; expect 503 + self.assertEqual(r.status_code, 503) + + self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) + self.cli_commit() + + r = request('POST', url, verify=False, headers=headers, data=payload) + # api configured; expect 200 + self.assertEqual(r.status_code, 200) + + self.cli_delete(base_path + ['api']) + self.cli_commit() + + r = request('POST', url, verify=False, headers=headers, data=payload) + # api deleted; expect 503 + self.assertEqual(r.status_code, 503) + + @ignore_warning(InsecureRequestWarning) def test_api_show(self): address = '127.0.0.1' key = 'VyOS-key' diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index 4dd3e761c..358668e0d 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-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,28 +17,35 @@ import re import unittest +from collections import OrderedDict from base_accel_ppp_test import BasicAccelPPPTest from vyos.configsession import ConfigSessionError from vyos.utils.process import cmd - from configparser import ConfigParser +from configparser import RawConfigParser -ac_name = 'ACN' -interface = 'eth0' +ac_name = "ACN" +interface = "eth0" -def getConfig(string, end='cli'): - command = f'cat /run/accel-pppd/ipoe.conf | sed -n "/^{string}/,/^{end}/p"' - out = cmd(command) - return out +class MultiOrderedDict(OrderedDict): + # Accel-ppp has duplicate keys in config file (gw-ip-address) + # This class is used to define dictionary which can contain multiple values + # in one key. + def __setitem__(self, key, value): + if isinstance(value, list) and key in self: + self[key].extend(value) + else: + super(OrderedDict, self).__setitem__(key, value) class TestServiceIPoEServer(BasicAccelPPPTest.TestCase): @classmethod def setUpClass(cls): - cls._base_path = ['service', 'ipoe-server'] - cls._config_file = '/run/accel-pppd/ipoe.conf' - cls._chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' + cls._base_path = ["service", "ipoe-server"] + cls._config_file = "/run/accel-pppd/ipoe.conf" + cls._chap_secrets = "/run/accel-pppd/ipoe.chap-secrets" + cls._protocol_section = "ipoe" # call base-classes classmethod super(TestServiceIPoEServer, cls).setUpClass() @@ -47,22 +54,29 @@ class TestServiceIPoEServer(BasicAccelPPPTest.TestCase): super().verify(conf) # Validate configuration values - accel_modules = list(conf['modules'].keys()) - self.assertIn('log_syslog', accel_modules) - self.assertIn('ipoe', accel_modules) - self.assertIn('shaper', accel_modules) - self.assertIn('ipv6pool', accel_modules) - self.assertIn('ipv6_nd', accel_modules) - self.assertIn('ipv6_dhcp', accel_modules) - self.assertIn('ippool', accel_modules) - - def basic_config(self): - self.set(['interface', interface, 'client-subnet', '192.168.0.0/24']) + accel_modules = list(conf["modules"].keys()) + self.assertIn("log_syslog", accel_modules) + self.assertIn("ipoe", accel_modules) + self.assertIn("shaper", accel_modules) + self.assertIn("ipv6pool", accel_modules) + self.assertIn("ipv6_nd", accel_modules) + self.assertIn("ipv6_dhcp", accel_modules) + self.assertIn("ippool", accel_modules) + + def initial_gateway_config(self): + self._gateway = "192.0.2.1/24" + super().initial_gateway_config() + + def initial_auth_config(self): + self.set(["authentication", "mode", "noauth"]) + + def basic_protocol_specific_config(self): + self.set(["interface", interface, "client-subnet", "192.168.0.0/24"]) def test_accel_local_authentication(self): - mac_address = '08:00:27:2f:d8:06' - self.set(['authentication', 'interface', interface, 'mac', mac_address]) - self.set(['authentication', 'mode', 'local']) + mac_address = "08:00:27:2f:d8:06" + self.set(["authentication", "interface", interface, "mac", mac_address]) + self.set(["authentication", "mode", "local"]) # No IPoE interface configured with self.assertRaises(ConfigSessionError): @@ -70,115 +84,109 @@ class TestServiceIPoEServer(BasicAccelPPPTest.TestCase): # Test configuration of local authentication for PPPoE server self.basic_config() - + # Rewrite authentication from basic_config + self.set(["authentication", "interface", interface, "mac", mac_address]) + self.set(["authentication", "mode", "local"]) # commit changes self.cli_commit() # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) conf.read(self._config_file) # check proper path to chap-secrets file - self.assertEqual(conf['chap-secrets']['chap-secrets'], self._chap_secrets) + self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets) - accel_modules = list(conf['modules'].keys()) - self.assertIn('chap-secrets', accel_modules) + accel_modules = list(conf["modules"].keys()) + self.assertIn("chap-secrets", accel_modules) # basic verification self.verify(conf) # check local users - tmp = cmd(f'sudo cat {self._chap_secrets}') - regex = f'{interface}\s+\*\s+{mac_address}\s+\*' + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{interface}\s+\*\s+{mac_address}\s+\*" tmp = re.findall(regex, tmp) self.assertTrue(tmp) - def test_accel_named_pool(self): - first_pool = 'VyOS-pool1' - first_subnet = '192.0.2.0/25' - first_gateway = '192.0.2.1' - second_pool = 'Vyos-pool2' - second_subnet = '203.0.113.0/25' - second_gateway = '203.0.113.1' - - self.set(['authentication', 'mode', 'noauth']) - self.set(['client-ip-pool', 'name', first_pool, 'gateway-address', first_gateway]) - self.set(['client-ip-pool', 'name', first_pool, 'subnet', first_subnet]) - self.set(['client-ip-pool', 'name', second_pool, 'gateway-address', second_gateway]) - self.set(['client-ip-pool', 'name', second_pool, 'subnet', second_subnet]) - self.set(['interface', interface]) + def test_accel_ipv4_pool(self): + self.basic_config(is_gateway=False, is_client_pool=False) + gateway = ["172.16.0.1/25", "192.0.2.1/24"] + subnet = "172.16.0.0/24" + first_pool = "POOL1" + second_pool = "POOL2" + range = "192.0.2.10-192.0.2.20" + + for gw in gateway: + self.set(["gateway-address", gw]) + + self.set(["client-ip-pool", first_pool, "range", subnet]) + self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) + self.set(["client-ip-pool", second_pool, "range", range]) + self.set(["default-pool", first_pool]) # commit changes - self.cli_commit() + self.cli_commit() # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf = RawConfigParser( + allow_no_value=True, + delimiters="=", + strict=False, + dict_type=MultiOrderedDict, + ) conf.read(self._config_file) - self.assertTrue(conf['ipoe']['interface'], f'{interface},shared=1,mode=L2,ifcfg=1,start=dhcpv4,ipv6=1') - self.assertTrue(conf['ipoe']['noauth'], '1') - self.assertTrue(conf['ipoe']['ip-pool'], first_pool) - self.assertTrue(conf['ipoe']['ip-pool'], second_pool) - self.assertTrue(conf['ipoe']['gw-ip-address'], f'{first_gateway}/25') - self.assertTrue(conf['ipoe']['gw-ip-address'], f'{second_gateway}/25') - - config = getConfig('[ip-pool]') - pool_config = f'''{second_subnet},name={second_pool} -{first_subnet},name={first_pool} -gw-ip-address={second_gateway}/25 -gw-ip-address={first_gateway}/25''' - self.assertIn(pool_config, config) + self.assertIn( + f"{first_pool},next={second_pool}", conf["ip-pool"][f"{subnet},name"] + ) + self.assertIn(second_pool, conf["ip-pool"][f"{range},name"]) + + gw_pool_config_list = conf.get("ip-pool", "gw-ip-address") + gw_ipoe_config_list = conf.get(self._protocol_section, "gw-ip-address") + for gw in gateway: + self.assertIn(gw.split("/")[0], gw_pool_config_list) + self.assertIn(gw, gw_ipoe_config_list) + self.assertIn(first_pool, conf[self._protocol_section]["ip-pool"]) def test_accel_next_pool(self): - first_pool = 'VyOS-pool1' - first_subnet = '192.0.2.0/25' - first_gateway = '192.0.2.1' - second_pool = 'Vyos-pool2' - second_subnet = '203.0.113.0/25' - second_gateway = '203.0.113.1' - third_pool = 'Vyos-pool3' - third_subnet = '198.51.100.0/24' - third_gateway = '198.51.100.1' - - self.set(['authentication', 'mode', 'noauth']) - self.set(['client-ip-pool', 'name', first_pool, 'gateway-address', first_gateway]) - self.set(['client-ip-pool', 'name', first_pool, 'subnet', first_subnet]) - self.set(['client-ip-pool', 'name', first_pool, 'next-pool', second_pool]) - self.set(['client-ip-pool', 'name', second_pool, 'gateway-address', second_gateway]) - self.set(['client-ip-pool', 'name', second_pool, 'subnet', second_subnet]) - self.set(['client-ip-pool', 'name', second_pool, 'next-pool', third_pool]) - self.set(['client-ip-pool', 'name', third_pool, 'gateway-address', third_gateway]) - self.set(['client-ip-pool', 'name', third_pool, 'subnet', third_subnet]) - self.set(['interface', interface]) + self.basic_config(is_gateway=False, is_client_pool=False) + + first_pool = "VyOS-pool1" + first_subnet = "192.0.2.0/25" + first_gateway = "192.0.2.1/24" + second_pool = "Vyos-pool2" + second_subnet = "203.0.113.0/25" + second_gateway = "203.0.113.1/24" + third_pool = "Vyos-pool3" + third_subnet = "198.51.100.0/24" + third_gateway = "198.51.100.1/24" + + self.set(["gateway-address", f"{first_gateway}"]) + self.set(["gateway-address", f"{second_gateway}"]) + self.set(["gateway-address", f"{third_gateway}"]) + + self.set(["client-ip-pool", first_pool, "range", first_subnet]) + self.set(["client-ip-pool", first_pool, "next-pool", second_pool]) + self.set(["client-ip-pool", second_pool, "range", second_subnet]) + self.set(["client-ip-pool", second_pool, "next-pool", third_pool]) + self.set(["client-ip-pool", third_pool, "range", third_subnet]) # commit changes self.cli_commit() - - # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) - conf.read(self._config_file) - - self.assertTrue(conf['ipoe']['interface'], f'{interface},shared=1,mode=L2,ifcfg=1,start=dhcpv4,ipv6=1') - self.assertTrue(conf['ipoe']['noauth'], '1') - self.assertTrue(conf['ipoe']['ip-pool'], first_pool) - self.assertTrue(conf['ipoe']['gw-ip-address'], f'{first_gateway}/25') - self.assertTrue(conf['ipoe']['gw-ip-address'], f'{second_gateway}/25') - self.assertTrue(conf['ipoe']['gw-ip-address'], f'{third_gateway}/24') - - config = getConfig('[ip-pool]') + config = self.getConfig("ip-pool") # T5099 required specific order - pool_config = f'''{third_subnet},name={third_pool} + pool_config = f"""gw-ip-address={first_gateway.split('/')[0]} +gw-ip-address={second_gateway.split('/')[0]} +gw-ip-address={third_gateway.split('/')[0]} +{third_subnet},name={third_pool} {second_subnet},name={second_pool},next={third_pool} -{first_subnet},name={first_pool},next={second_pool} -gw-ip-address={third_gateway}/24 -gw-ip-address={second_gateway}/25 -gw-ip-address={first_gateway}/25''' +{first_subnet},name={first_pool},next={second_pool}""" self.assertIn(pool_config, config) -if __name__ == '__main__': +if __name__ == "__main__": unittest.main(verbosity=2) - diff --git a/smoketest/scripts/cli/test_service_pppoe-server.py b/smoketest/scripts/cli/test_service_pppoe-server.py index 969abd3d5..3001e71bf 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -32,7 +32,7 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): cls._base_path = ['service', 'pppoe-server'] cls._config_file = '/run/accel-pppd/pppoe.conf' cls._chap_secrets = '/run/accel-pppd/pppoe.chap-secrets' - + cls._protocol_section = 'pppoe' # call base-classes classmethod super(TestServicePPPoEServer, cls).setUpClass() @@ -65,13 +65,11 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): super().verify(conf) - def basic_config(self): + def basic_protocol_specific_config(self): self.cli_set(local_if + ['address', '192.0.2.1/32']) - self.set(['access-concentrator', ac_name]) self.set(['interface', interface]) - super().basic_config() def test_pppoe_server_ppp_options(self): # Test configuration of local authentication for PPPoE server @@ -120,7 +118,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): # check interface-cache self.assertEqual(conf['ppp']['unit-cache'], interface_cache) - def test_pppoe_server_authentication_protocols(self): # Test configuration of local authentication for PPPoE server self.basic_config() @@ -137,68 +134,25 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['modules']['auth_mschap_v2'], None) - - def test_pppoe_server_client_ip_pool(self): - # Test configuration of IPv6 client pools - self.basic_config() - - subnet = '172.18.0.0/24' + def test_pppoe_server_shaper(self): fwmark = '223' limiter = 'tbf' + self.basic_config() - self.set(['client-ip-pool', 'subnet', subnet]) - - start = '192.0.2.10' - stop = '192.0.2.20' - stop_octet = stop.split('.')[3] - start_stop = f'{start}-{stop_octet}' - self.set(['client-ip-pool', 'start', start]) - self.set(['client-ip-pool', 'stop', stop]) self.set(['shaper', 'fwmark', fwmark]) - # commit changes - self.cli_commit() - - # Validate configuration values - conf = ConfigParser(allow_no_value=True) - conf.read(self._config_file) - # check configured subnet - self.assertEqual(conf['ip-pool'][subnet], None) - self.assertEqual(conf['ip-pool'][start_stop], None) - self.assertEqual(conf['ip-pool']['gw-ip-address'], self._gateway) - self.assertEqual(conf['shaper']['fwmark'], fwmark) - self.assertEqual(conf['shaper']['down-limiter'], limiter) - - - def test_pppoe_server_client_ip_pool_name(self): - # Test configuration of named client pools - self.basic_config() - - subnet = '192.0.2.0/24' - gateway = '192.0.2.1' - pool = 'VYOS' - - subnet_name = f'{subnet},name' - gw_ip_prefix = f'{gateway}/24' - - self.set(['client-ip-pool', 'name', pool, 'subnet', subnet]) - self.set(['client-ip-pool', 'name', pool, 'gateway-address', gateway]) - self.cli_delete(self._base_path + ['gateway-address']) - - # commit changes self.cli_commit() # Validate configuration values conf = ConfigParser(allow_no_value=True, delimiters='=') conf.read(self._config_file) - # Validate configuration - self.assertEqual(conf['ip-pool'][subnet_name], pool) - self.assertEqual(conf['ip-pool']['gw-ip-address'], gateway) - self.assertEqual(conf['pppoe']['ip-pool'], pool) - self.assertEqual(conf['pppoe']['gw-ip-address'], gw_ip_prefix) + # basic verification + self.verify(conf) + self.assertEqual(conf['shaper']['fwmark'], fwmark) + self.assertEqual(conf['shaper']['down-limiter'], limiter) def test_pppoe_server_client_ipv6_pool(self): # Test configuration of IPv6 client pools @@ -239,7 +193,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['ipv6-pool'][client_prefix], None) self.assertEqual(conf['ipv6-pool']['delegate'], f'{delegate_prefix},{delegate_mask}') - def test_accel_radius_authentication(self): radius_called_sid = 'ifname:mac' radius_acct_interim_jitter = '9' @@ -261,7 +214,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['radius']['acct-interim-jitter'], radius_acct_interim_jitter) self.assertEqual(conf['radius']['acct-interim-interval'], radius_acct_interim_interval) - def test_pppoe_server_vlan(self): vlans = ['100', '200', '300-310'] @@ -284,5 +236,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): tmp = ','.join(vlans) self.assertIn(f'vlan-mon={interface},{tmp}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index 7657ab724..0dbc97d49 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -297,5 +297,49 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): self.cli_delete(['firewall']) + def test_conntrack_timeout_custom(self): + + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'source', 'address', '192.0.2.1']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'destination', 'address', '192.0.2.2']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'destination', 'port', '22']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'syn-sent', '77']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'close', '88']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '1', 'protocol', 'tcp', 'established', '99']) + + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'inbound-interface', 'eth1']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'source', 'address', '198.51.100.1']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv4', 'rule', '2', 'protocol', 'udp', 'unreplied', '55']) + + self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'source', 'address', '2001:db8::1']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'inbound-interface', 'eth2']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'protocol', 'tcp', 'time-wait', '22']) + self.cli_set(base_path + ['timeout', 'custom', 'ipv6', 'rule', '1', 'protocol', 'tcp', 'last-ack', '33']) + + self.cli_commit() + + nftables_search = [ + ['ct timeout ct-timeout-1 {'], + ['protocol tcp'], + ['policy = { syn_sent : 77, established : 99, close : 88 }'], + ['ct timeout ct-timeout-2 {'], + ['protocol udp'], + ['policy = { unreplied : 55 }'], + ['chain VYOS_CT_TIMEOUT {'], + ['ip saddr 192.0.2.1', 'ip daddr 192.0.2.2', 'tcp dport 22', 'ct timeout set "ct-timeout-1"'], + ['iifname "eth1"', 'meta l4proto udp', 'ip saddr 198.51.100.1', 'ct timeout set "ct-timeout-2"'] + ] + + nftables6_search = [ + ['ct timeout ct-timeout-1 {'], + ['protocol tcp'], + ['policy = { last_ack : 33, time_wait : 22 }'], + ['chain VYOS_CT_TIMEOUT {'], + ['iifname "eth2"', 'meta l4proto tcp', 'ip6 saddr 2001:db8::1', 'ct timeout set "ct-timeout-1"'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_conntrack') + self.verify_nftables(nftables6_search, 'ip6 vyos_conntrack') + + self.cli_delete(['firewall']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py new file mode 100755 index 000000000..05ffb6bb5 --- /dev/null +++ b/smoketest/scripts/cli/test_vpn_l2tp.py @@ -0,0 +1,212 @@ +#!/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/>. +import re +import unittest + +from base_accel_ppp_test import BasicAccelPPPTest +from configparser import ConfigParser +from vyos.utils.process import cmd + + +class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): + @classmethod + def setUpClass(cls): + cls._base_path = ['vpn', 'l2tp', 'remote-access'] + cls._config_file = '/run/accel-pppd/l2tp.conf' + cls._chap_secrets = '/run/accel-pppd/l2tp.chap-secrets' + cls._protocol_section = 'l2tp' + # call base-classes classmethod + super(TestVPNL2TPServer, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + super(TestVPNL2TPServer, cls).tearDownClass() + + def basic_protocol_specific_config(self): + pass + + def test_accel_local_authentication(self): + # Test configuration of local authentication + self.basic_config() + + # upload / download limit + user = "test" + password = "test2" + static_ip = "100.100.100.101" + upload = "5000" + download = "10000" + + self.set( + [ + "authentication", + "local-users", + "username", + user, + "password", + password, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "static-ip", + static_ip, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "rate-limit", + "upload", + upload, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "rate-limit", + "download", + download, + ] + ) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + # check proper path to chap-secrets file + self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets) + + # basic verification + self.verify(conf) + + # check local users + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+{static_ip}\s+{download}/{upload}" + tmp = re.findall(regex, tmp) + self.assertTrue(tmp) + + # Check local-users default value(s) + self.delete(["authentication", "local-users", "username", user, "static-ip"]) + # commit changes + self.cli_commit() + + # check local users + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+\*\s+{download}/{upload}" + tmp = re.findall(regex, tmp) + self.assertTrue(tmp) + + def test_accel_radius_authentication(self): + # Test configuration of RADIUS authentication for PPPoE server + self.basic_config() + + radius_server = "192.0.2.22" + radius_key = "secretVyOS" + radius_port = "2000" + + self.set(["authentication", "mode", "radius"]) + self.set( + ["authentication", "radius", "server", radius_server, "key", radius_key] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "port", + radius_port, + ] + ) + + + nas_id = "VyOS-PPPoE" + nas_ip = "7.7.7.7" + self.set(["authentication", "radius", "nas-identifier", nas_id]) + self.set(["authentication", "radius", "nas-ip-address", nas_ip]) + + source_address = "1.2.3.4" + self.set(["authentication", "radius", "source-address", source_address]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + # basic verification + self.verify(conf) + + # check auth + self.assertTrue(conf["radius"].getboolean("verbose")) + self.assertEqual(conf["radius"]["acct-timeout"], "3") + self.assertEqual(conf["radius"]["timeout"], "3") + self.assertEqual(conf["radius"]["max-try"], "3") + + self.assertEqual(conf["radius"]["nas-identifier"], nas_id) + self.assertEqual(conf["radius"]["nas-ip-address"], nas_ip) + self.assertEqual(conf["radius"]["bind"], source_address) + + server = conf["radius"]["server"].split(",") + self.assertEqual(radius_server, server[0]) + self.assertEqual(radius_key, server[1]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) + + # + # Disable Radius Accounting + # + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "disable-accounting", + ] + ) + + # commit changes + self.cli_commit() + + conf.read(self._config_file) + + server = conf["radius"]["server"].split(",") + self.assertEqual(radius_server, server[0]) + self.assertEqual(radius_key, server[1]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"acct-port=0", server[3]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_pptp.py b/smoketest/scripts/cli/test_vpn_pptp.py new file mode 100755 index 000000000..0d9ea312e --- /dev/null +++ b/smoketest/scripts/cli/test_vpn_pptp.py @@ -0,0 +1,223 @@ +#!/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/>. + +import re +import unittest + +from configparser import ConfigParser +from vyos.utils.process import cmd +from base_accel_ppp_test import BasicAccelPPPTest +from vyos.template import is_ipv4 + + +class TestVPNPPTPServer(BasicAccelPPPTest.TestCase): + @classmethod + def setUpClass(cls): + cls._base_path = ['vpn', 'pptp', 'remote-access'] + cls._config_file = '/run/accel-pppd/pptp.conf' + cls._chap_secrets = '/run/accel-pppd/pptp.chap-secrets' + cls._protocol_section = 'pptp' + # call base-classes classmethod + super(TestVPNPPTPServer, cls).setUpClass() + + @classmethod + def tearDownClass(cls): + super(TestVPNPPTPServer, cls).tearDownClass() + + def basic_protocol_specific_config(self): + pass + + def test_accel_name_servers(self): + # Verify proper Name-Server configuration for IPv4 + self.basic_config() + + nameserver = ["192.0.2.1", "192.0.2.2"] + for ns in nameserver: + self.set(["name-server", ns]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + # IPv4 and IPv6 nameservers must be checked individually + for ns in nameserver: + self.assertIn(ns, [conf["dns"]["dns1"], conf["dns"]["dns2"]]) + + def test_accel_local_authentication(self): + # Test configuration of local authentication + self.basic_config() + + # upload / download limit + user = "test" + password = "test2" + static_ip = "100.100.100.101" + upload = "5000" + download = "10000" + + self.set( + [ + "authentication", + "local-users", + "username", + user, + "password", + password, + ] + ) + self.set( + [ + "authentication", + "local-users", + "username", + user, + "static-ip", + static_ip, + ] + ) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + # check proper path to chap-secrets file + self.assertEqual(conf["chap-secrets"]["chap-secrets"], self._chap_secrets) + + # basic verification + self.verify(conf) + + # check local users + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+{static_ip}\s" + tmp = re.findall(regex, tmp) + self.assertTrue(tmp) + + # Check local-users default value(s) + self.delete(["authentication", "local-users", "username", user, "static-ip"]) + # commit changes + self.cli_commit() + + # check local users + tmp = cmd(f"sudo cat {self._chap_secrets}") + regex = f"{user}\s+\*\s+{password}\s+\*\s" + tmp = re.findall(regex, tmp) + self.assertTrue(tmp) + + def test_accel_radius_authentication(self): + # Test configuration of RADIUS authentication for PPPoE server + self.basic_config() + + radius_server = "192.0.2.22" + radius_key = "secretVyOS" + radius_port = "2000" + radius_port_acc = "3000" + + self.set(["authentication", "mode", "radius"]) + self.set( + ["authentication", "radius", "server", radius_server, "key", radius_key] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "port", + radius_port, + ] + ) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "acct-port", + radius_port_acc, + ] + ) + + nas_id = "VyOS-PPPoE" + nas_ip = "7.7.7.7" + self.set(["authentication", "radius", "nas-identifier", nas_id]) + self.set(["authentication", "radius", "nas-ip-address", nas_ip]) + + source_address = "1.2.3.4" + self.set(["authentication", "radius", "source-address", source_address]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False) + conf.read(self._config_file) + + # basic verification + self.verify(conf) + + # check auth + self.assertTrue(conf["radius"].getboolean("verbose")) + self.assertEqual(conf["radius"]["acct-timeout"], "30") + self.assertEqual(conf["radius"]["timeout"], "30") + self.assertEqual(conf["radius"]["max-try"], "3") + + self.assertEqual(conf["radius"]["nas-identifier"], nas_id) + self.assertEqual(conf["radius"]["nas-ip-address"], nas_ip) + self.assertEqual(conf["radius"]["bind"], source_address) + + server = conf["radius"]["server"].split(",") + self.assertEqual(radius_server, server[0]) + self.assertEqual(radius_key, server[1]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"acct-port={radius_port_acc}", server[3]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) + + # + # Disable Radius Accounting + # + self.delete(["authentication", "radius", "server", radius_server, "acct-port"]) + self.set( + [ + "authentication", + "radius", + "server", + radius_server, + "disable-accounting", + ] + ) + + # commit changes + self.cli_commit() + + conf.read(self._config_file) + + server = conf["radius"]["server"].split(",") + self.assertEqual(radius_server, server[0]) + self.assertEqual(radius_key, server[1]) + self.assertEqual(f"auth-port={radius_port}", server[2]) + self.assertEqual(f"acct-port=0", server[3]) + self.assertEqual(f"req-limit=0", server[4]) + self.assertEqual(f"fail-time=0", server[5]) + + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_sstp.py b/smoketest/scripts/cli/test_vpn_sstp.py index 232eafcf2..f0695d577 100755 --- a/smoketest/scripts/cli/test_vpn_sstp.py +++ b/smoketest/scripts/cli/test_vpn_sstp.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 @@ -47,7 +47,7 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): cls._base_path = ['vpn', 'sstp'] cls._config_file = '/run/accel-pppd/sstp.conf' cls._chap_secrets = '/run/accel-pppd/sstp.chap-secrets' - + cls._protocol_section = 'sstp' # call base-classes classmethod super(TestVPNSSTPServer, cls).setUpClass() @@ -58,26 +58,23 @@ class TestVPNSSTPServer(BasicAccelPPPTest.TestCase): @classmethod def tearDownClass(cls): cls.cli_delete(cls, pki_path) - super(TestVPNSSTPServer, cls).tearDownClass() - def basic_config(self): - # SSL is mandatory + def basic_protocol_specific_config(self): self.set(['ssl', 'ca-certificate', 'sstp']) self.set(['ssl', 'certificate', 'sstp']) - self.set(['client-ip-pool', 'subnet', '192.0.2.0/24']) - - super().basic_config() def test_accel_local_authentication(self): # Change default port port = '8443' self.set(['port', port]) + self.basic_config() super().test_accel_local_authentication() config = read_file(self._config_file) self.assertIn(f'port={port}', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vrf.py b/smoketest/scripts/cli/test_vrf.py index 0f658f366..bb91eddea 100755 --- a/smoketest/scripts/cli/test_vrf.py +++ b/smoketest/scripts/cli/test_vrf.py @@ -489,4 +489,4 @@ class VRFTest(VyOSUnitTestSHIM.TestCase): if __name__ == '__main__': - unittest.main(verbosity=2, failfast=True) + unittest.main(verbosity=2) diff --git a/src/completion/list_ddclient_protocols.sh b/src/completion/list_ddclient_protocols.sh index c8855b5d1..634981660 100755 --- a/src/completion/list_ddclient_protocols.sh +++ b/src/completion/list_ddclient_protocols.sh @@ -14,4 +14,4 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -echo -n $(ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun') +echo -n $(ddclient -list-protocols | grep -vE 'cloudns|porkbun') diff --git a/src/conf_mode/conntrack.py b/src/conf_mode/conntrack.py index 4cece6921..7f6c71440 100755 --- a/src/conf_mode/conntrack.py +++ b/src/conf_mode/conntrack.py @@ -159,6 +159,13 @@ def verify(conntrack): if not group_obj: Warning(f'{error_group} "{group_name}" has no members!') + if dict_search_args(conntrack, 'timeout', 'custom', inet, 'rule') != None: + for rule, rule_config in conntrack['timeout']['custom'][inet]['rule'].items(): + if 'protocol' not in rule_config: + raise ConfigError(f'Conntrack custom timeout rule {rule} requires protocol tcp or udp') + else: + if 'tcp' in rule_config['protocol'] and 'udp' in rule_config['protocol']: + raise ConfigError(f'conntrack custom timeout rule {rule} - Cant use both tcp and udp protocol') return None def generate(conntrack): diff --git a/src/conf_mode/dns_dynamic.py b/src/conf_mode/dns_dynamic.py index 2bccaee0f..3ddc8e7fd 100755 --- a/src/conf_mode/dns_dynamic.py +++ b/src/conf_mode/dns_dynamic.py @@ -30,16 +30,18 @@ config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' # Protocols that require zone -zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn'] +zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', + 'nfsn', 'nsupdate'] zone_supported = zone_necessary + ['dnsexit2', 'zoneedit1'] # Protocols that do not require username username_unnecessary = ['1984', 'cloudflare', 'cloudns', 'digitalocean', 'dnsexit2', 'duckdns', 'freemyip', 'hetzner', 'keysystems', 'njalla', - 'regfishde'] + 'nsupdate', 'regfishde'] # Protocols that support TTL -ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn'] +ttl_supported = ['cloudflare', 'dnsexit2', 'gandi', 'hetzner', 'godaddy', 'nfsn', + 'nsupdate'] # Protocols that support both IPv4 and IPv6 dualstack_supported = ['cloudflare', 'digitalocean', 'dnsexit2', 'duckdns', @@ -70,63 +72,65 @@ def get_config(config=None): def verify(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None - for address in dyndns['address']: - # If dyndns address is an interface, ensure it exists - if address != 'web': - verify_interface_exists(address) + # Dynamic DNS service provider - configuration validation + for service, config in dyndns['name'].items(): - # 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}"') + error_msg_req = f'is required for Dynamic DNS service "{service}"' + error_msg_uns = f'is not supported for Dynamic DNS service "{service}"' - # Dynamic DNS service provider - configuration validation - if 'web_options' in dyndns['address'][address] and address != 'web': - raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') + for field in ['protocol', 'address', 'host_name']: + if field not in config: + raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # Dynamic DNS service provider - configuration validation - if 'service' in dyndns['address'][address]: - for service, config in dyndns['address'][address]['service'].items(): - error_msg_req = f'is required for Dynamic DNS service "{service}" on "{address}"' - error_msg_uns = f'is not supported for Dynamic DNS service "{service}" on "{address}" with protocol "{config["protocol"]}"' + # If dyndns address is an interface, ensure that it exists + # and that web-options are not set + if config['address'] != 'web': + verify_interface_exists(config['address']) + if 'web_options' in config: + raise ConfigError(f'"web-options" is applicable only when using HTTP(S) web request to obtain the IP address') - for field in ['host_name', 'password', 'protocol']: - if field not in config: - raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') + # RFC2136 uses 'key' instead of 'password' + if config['protocol'] != 'nsupdate' and 'password' not in config: + raise ConfigError(f'"password" {error_msg_req}') - if config['protocol'] in zone_necessary and 'zone' not in config: - raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') + # Other RFC2136 specific configuration validation + if config['protocol'] == 'nsupdate': + if 'password' in config: + raise ConfigError(f'"password" {error_msg_uns} with protocol "{config["protocol"]}"') + for field in ['server', 'key']: + if field not in config: + raise ConfigError(f'"{field}" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in zone_supported and 'zone' in config: - raise ConfigError(f'"zone" {error_msg_uns}') + if config['protocol'] in zone_necessary and 'zone' not in config: + raise ConfigError(f'"zone" {error_msg_req} with protocol "{config["protocol"]}"') - if config['protocol'] not in username_unnecessary and 'username' not in config: - raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') + if config['protocol'] not in zone_supported and 'zone' in config: + raise ConfigError(f'"zone" {error_msg_uns} with protocol "{config["protocol"]}"') - if config['protocol'] not in ttl_supported and 'ttl' in config: - raise ConfigError(f'"ttl" {error_msg_uns}') + if config['protocol'] not in username_unnecessary and 'username' not in config: + raise ConfigError(f'"username" {error_msg_req} with protocol "{config["protocol"]}"') - if config['ip_version'] == 'both': - if config['protocol'] not in dualstack_supported: - raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns}') - # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) - if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: - raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}"') + if config['protocol'] not in ttl_supported and 'ttl' in config: + raise ConfigError(f'"ttl" {error_msg_uns} with protocol "{config["protocol"]}"') - if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): - raise ConfigError(f'"expiry-time" must be greater than "wait-time"') + if config['ip_version'] == 'both': + if config['protocol'] not in dualstack_supported: + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} with protocol "{config["protocol"]}"') + # dyndns2 protocol in ddclient honors dual stack only for dyn.com (dyndns.org) + if config['protocol'] == 'dyndns2' and 'server' in config and config['server'] not in dyndns_dualstack_servers: + raise ConfigError(f'Both IPv4 and IPv6 at the same time {error_msg_uns} for "{config["server"]}" with protocol "{config["protocol"]}"') + + if {'wait_time', 'expiry_time'} <= config.keys() and int(config['expiry_time']) < int(config['wait_time']): + raise ConfigError(f'"expiry-time" must be greater than "wait-time" for Dynamic DNS service "{service}"') return None def generate(dyndns): # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: return None render(config_file, 'dns-dynamic/ddclient.conf.j2', dyndns, permission=0o600) @@ -139,7 +143,7 @@ def apply(dyndns): call('systemctl daemon-reload') # bail out early - looks like removal from running config - if not dyndns or 'address' not in dyndns: + if not dyndns or 'name' not in dyndns: call(f'systemctl stop {systemd_service}') if os.path.exists(config_file): os.unlink(config_file) diff --git a/src/conf_mode/http-api.py b/src/conf_mode/http-api.py deleted file mode 100755 index 855d444c6..000000000 --- a/src/conf_mode/http-api.py +++ /dev/null @@ -1,112 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2021 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys -import os -import json - -from time import sleep - -import vyos.defaults - -from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents -from vyos.template import render -from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running -from vyos import ConfigError -from vyos import airbag -airbag.enable() - -api_config_state = '/tmp/api-config-state' -systemd_service = '/run/systemd/system/vyos-http-api.service' - -vyos_conf_scripts_dir=vyos.defaults.directories['conf_mode'] - -def get_config(config=None): - if config: - conf = config - else: - conf = Config() - - # reset on creation/deletion of 'api' node - https_base = ['service', 'https'] - if conf.exists(https_base): - set_dependents("https", conf) - - base = ['service', 'https', 'api'] - if not conf.exists(base): - return None - - http_api = conf.get_config_dict(base, key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - # Do we run inside a VRF context? - vrf_path = ['service', 'https', 'vrf'] - if conf.exists(vrf_path): - http_api['vrf'] = conf.return_value(vrf_path) - - if http_api.from_defaults(['graphql']): - del http_api['graphql'] - - return http_api - -def verify(_http_api): - return - -def generate(http_api): - if http_api is None: - if os.path.exists(systemd_service): - os.unlink(systemd_service) - return - - with open(api_config_state, 'w') as f: - json.dump(http_api, f, indent=2) - - render(systemd_service, 'https/vyos-http-api.service.j2', http_api) - -def apply(http_api): - # Reload systemd manager configuration - call('systemctl daemon-reload') - service_name = 'vyos-http-api.service' - - if http_api is not None: - if is_systemd_service_running(f'{service_name}'): - call(f'systemctl reload {service_name}') - else: - call(f'systemctl restart {service_name}') - else: - call(f'systemctl stop {service_name}') - - # Let uvicorn settle before restarting Nginx - sleep(1) - - call_dependents() - - if os.path.exists(api_config_state): - os.unlink(api_config_state) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/https.py b/src/conf_mode/https.py index 81e510b0d..40b7de557 100755 --- a/src/conf_mode/https.py +++ b/src/conf_mode/https.py @@ -16,19 +16,24 @@ import os import sys +import json from copy import deepcopy +from time import sleep import vyos.defaults import vyos.certbot_util from vyos.config import Config +from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf from vyos import ConfigError from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.process import call +from vyos.utils.process import is_systemd_service_running +from vyos.utils.process import is_systemd_service_active from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file @@ -42,6 +47,9 @@ cert_dir = '/etc/ssl/certs' key_dir = '/etc/ssl/private' certbot_dir = vyos.defaults.directories['certbot'] +api_config_state = '/run/http-api-state' +systemd_service = '/run/systemd/system/vyos-http-api.service' + # https config needs to coordinate several subsystems: api, certbot, # self-signed certificate, as well as the virtual hosts defined within the # https config definition itself. Consequently, one needs a general dict, @@ -67,11 +75,35 @@ def get_config(config=None): if not conf.exists(base): return None + diff = get_config_diff(conf) + https = conf.get_config_dict(base, get_first_key=True) if https: https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), - get_first_key=True, no_tag_node_value_mangle=True) + no_tag_node_value_mangle=True, + get_first_key=True) + + https['children_changed'] = diff.node_changed_children(base) + https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) + + if 'api' not in https: + return https + + http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), + no_tag_node_value_mangle=True, + get_first_key=True, + with_recursive_defaults=True) + + if http_api.from_defaults(['graphql']): + del http_api['graphql'] + + # Do we run inside a VRF context? + vrf_path = ['service', 'https', 'vrf'] + if conf.exists(vrf_path): + http_api['vrf'] = conf.return_value(vrf_path) + + https['api'] = http_api return https @@ -103,7 +135,7 @@ def verify(https): if 'certbot' in https['certificates']: vhost_names = [] - for vh, vh_conf in https.get('virtual-host', {}).items(): + for _, vh_conf in https.get('virtual-host', {}).items(): vhost_names += vh_conf.get('server-name', []) domains = https['certificates']['certbot'].get('domain-name', []) domains_found = [domain for domain in domains if domain in vhost_names] @@ -167,6 +199,14 @@ def generate(https): if https is None: return None + if 'api' not in https: + if os.path.exists(systemd_service): + os.unlink(systemd_service) + else: + render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) + with open(api_config_state, 'w') as f: + json.dump(https['api'], f, indent=2) + server_block_list = [] # organize by vhosts @@ -254,10 +294,31 @@ def generate(https): def apply(https): # Reload systemd manager configuration call('systemctl daemon-reload') - if https is not None: - call('systemctl restart nginx.service') - else: - call('systemctl stop nginx.service') + http_api_service_name = 'vyos-http-api.service' + https_service_name = 'nginx.service' + + if https is None: + if is_systemd_service_active(f'{http_api_service_name}'): + call(f'systemctl stop {http_api_service_name}') + call(f'systemctl stop {https_service_name}') + return + + if 'api' in https['children_changed']: + if 'api' in https: + if is_systemd_service_running(f'{http_api_service_name}'): + call(f'systemctl reload {http_api_service_name}') + else: + call(f'systemctl restart {http_api_service_name}') + # Let uvicorn settle before (possibly) restarting nginx + sleep(1) + else: + if is_systemd_service_active(f'{http_api_service_name}'): + call(f'systemctl stop {http_api_service_name}') + + if (not is_systemd_service_running(f'{https_service_name}') or + https['api_add_or_delete'] or + set(https['children_changed']) - set(['api'])): + call(f'systemctl restart {https_service_name}') if __name__ == '__main__': try: diff --git a/src/conf_mode/nat64.py b/src/conf_mode/nat64.py new file mode 100755 index 000000000..a8b90fb11 --- /dev/null +++ b/src/conf_mode/nat64.py @@ -0,0 +1,209 @@ +#!/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/>. + +# pylint: disable=empty-docstring,missing-module-docstring + +import csv +import os +import re + +from ipaddress import IPv6Network +from json import dumps as json_write + +from vyos import ConfigError +from vyos import airbag +from vyos.config import Config +from vyos.configdict import dict_merge +from vyos.configdict import is_node_changed +from vyos.utils.dict import dict_search +from vyos.utils.file import write_file +from vyos.utils.kernel import check_kmod +from vyos.utils.process import cmd +from vyos.utils.process import run + +airbag.enable() + +INSTANCE_REGEX = re.compile(r"instance-(\d+)") +JOOL_CONFIG_DIR = "/run/jool" + + +def get_config(config: Config | None = None) -> None: + if config is None: + config = Config() + + base = ["nat64"] + nat64 = config.get_config_dict(base, key_mangling=("-", "_"), get_first_key=True) + + base_src = base + ["source", "rule"] + + # Load in existing instances so we can destroy any unknown + lines = cmd("jool instance display --csv").splitlines() + for _, instance, _ in csv.reader(lines): + match = INSTANCE_REGEX.fullmatch(instance) + if not match: + # FIXME: Instances that don't match should be ignored but WARN'ed to the user + continue + num = match.group(1) + + rules = nat64.setdefault("source", {}).setdefault("rule", {}) + # Mark it for deletion + if num not in rules: + rules[num] = {"deleted": True} + continue + + # If the user changes the mode, recreate the instance else Jool fails with: + # Jool error: Sorry; you can't change an instance's framework for now. + if is_node_changed(config, base_src + [f"instance-{num}", "mode"]): + rules[num]["recreate"] = True + + # If the user changes the pool6, recreate the instance else Jool fails with: + # Jool error: Sorry; you can't change a NAT64 instance's pool6 for now. + if dict_search("source.prefix", rules[num]) and is_node_changed( + config, + base_src + [num, "source", "prefix"], + ): + rules[num]["recreate"] = True + + return nat64 + + +def verify(nat64) -> None: + if not nat64: + # no need to verify the CLI as nat64 is going to be deactivated + return + + if dict_search("source.rule", nat64): + # Ensure only 1 netfilter instance per namespace + nf_rules = filter( + lambda i: "deleted" not in i and i.get('mode') == "netfilter", + nat64["source"]["rule"].values(), + ) + next(nf_rules, None) # Discard the first element + if next(nf_rules, None) is not None: + raise ConfigError( + "Jool permits only 1 NAT64 netfilter instance (per network namespace)" + ) + + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + continue + + # Verify that source.prefix is set and is a /96 + if not dict_search("source.prefix", instance): + raise ConfigError(f"Source NAT64 rule {rule} missing source prefix") + if IPv6Network(instance["source"]["prefix"]).prefixlen != 96: + raise ConfigError(f"Source NAT64 rule {rule} source prefix must be /96") + + pools = dict_search("translation.pool", instance) + if pools: + for num, pool in pools.items(): + if "address" not in pool: + raise ConfigError( + f"Source NAT64 rule {rule} translation pool " + f"{num} missing address/prefix" + ) + if "port" not in pool: + raise ConfigError( + f"Source NAT64 rule {rule} translation pool " + f"{num} missing port(-range)" + ) + + +def generate(nat64) -> None: + os.makedirs(JOOL_CONFIG_DIR, exist_ok=True) + + if dict_search("source.rule", nat64): + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + # Delete the unused instance file + os.unlink(os.path.join(JOOL_CONFIG_DIR, f"instance-{rule}.json")) + continue + + name = f"instance-{rule}" + config = { + "instance": name, + "framework": "netfilter", + "global": { + "pool6": instance["source"]["prefix"], + "manually-enabled": "disable" not in instance, + }, + # "bib": [], + } + + if "description" in instance: + config["comment"] = instance["description"] + + if dict_search("translation.pool", instance): + pool4 = [] + for pool in instance["translation"]["pool"].values(): + if "disable" in pool: + continue + + protos = pool.get("protocol", {}).keys() or ("tcp", "udp", "icmp") + for proto in protos: + obj = { + "protocol": proto.upper(), + "prefix": pool["address"], + "port range": pool["port"], + } + if "description" in pool: + obj["comment"] = pool["description"] + + pool4.append(obj) + + if pool4: + config["pool4"] = pool4 + + write_file(f'{JOOL_CONFIG_DIR}/{name}.json', json_write(config, indent=2)) + + +def apply(nat64) -> None: + if not nat64: + return + + if dict_search("source.rule", nat64): + # Deletions first to avoid conflicts + for rule, instance in nat64["source"]["rule"].items(): + if not any(k in instance for k in ("deleted", "recreate")): + continue + + ret = run(f"jool instance remove instance-{rule}") + if ret != 0: + raise ConfigError( + f"Failed to remove nat64 source rule {rule} (jool instance instance-{rule})" + ) + + # Now creations + for rule, instance in nat64["source"]["rule"].items(): + if "deleted" in instance: + continue + + name = f"instance-{rule}" + ret = run(f"jool -i {name} file handle {JOOL_CONFIG_DIR}/{name}.json") + if ret != 0: + raise ConfigError(f"Failed to set jool instance {name}") + + +if __name__ == "__main__": + try: + check_kmod(["jool"]) + c = get_config() + verify(c) + generate(c) + apply(c) + except ConfigError as e: + print(e) + exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index b70e32373..36f00dec5 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -15,17 +15,17 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os -import jmespath from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict -from vyos.configverify import verify_accel_ppp_base_service from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import get_pools_in_order +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool from vyos import ConfigError from vyos import airbag airbag.enable() @@ -35,87 +35,6 @@ ipoe_conf = '/run/accel-pppd/ipoe.conf' ipoe_chap_secrets = '/run/accel-pppd/ipoe.chap-secrets' -def get_pools_in_order(data: dict) -> list: - """Return a list of dictionaries representing pool data in the order - in which they should be allocated. Pool must be defined before we can - use it with 'next-pool' option. - - Args: - data: A dictionary of pool data, where the keys are pool names and the - values are dictionaries containing the 'subnet' key and the optional - 'next_pool' key. - - Returns: - list: A list of dictionaries - - Raises: - ValueError: If a 'next_pool' key references a pool name that - has not been defined. - ValueError: If a circular reference is found in the 'next_pool' keys. - - Example: - config_data = { - ... 'first-pool': { - ... 'next_pool': 'second-pool', - ... 'subnet': '192.0.2.0/25' - ... }, - ... 'second-pool': { - ... 'next_pool': 'third-pool', - ... 'subnet': '203.0.113.0/25' - ... }, - ... 'third-pool': { - ... 'subnet': '198.51.100.0/24' - ... }, - ... 'foo': { - ... 'subnet': '100.64.0.0/24', - ... 'next_pool': 'second-pool' - ... } - ... } - - % get_pools_in_order(config_data) - [{'third-pool': {'subnet': '198.51.100.0/24'}}, - {'second-pool': {'next_pool': 'third-pool', 'subnet': '203.0.113.0/25'}}, - {'first-pool': {'next_pool': 'second-pool', 'subnet': '192.0.2.0/25'}}, - {'foo': {'next_pool': 'second-pool', 'subnet': '100.64.0.0/24'}}] - """ - pools = [] - unresolved_pools = {} - - for pool, pool_config in data.items(): - if 'next_pool' not in pool_config: - pools.insert(0, {pool: pool_config}) - else: - unresolved_pools[pool] = pool_config - - while unresolved_pools: - resolved_pools = [] - - for pool, pool_config in unresolved_pools.items(): - next_pool_name = pool_config['next_pool'] - - if any(p for p in pools if next_pool_name in p): - index = next( - (i for i, p in enumerate(pools) if next_pool_name in p), - None) - pools.insert(index + 1, {pool: pool_config}) - resolved_pools.append(pool) - elif next_pool_name in unresolved_pools: - # next pool not yet resolved - pass - else: - raise ValueError( - f"Pool '{next_pool_name}' not defined in configuration data" - ) - - if not resolved_pools: - raise ValueError("Circular reference in configuration data") - - for pool in resolved_pools: - unresolved_pools.pop(pool) - - return pools - - def get_config(config=None): if config: conf = config @@ -128,18 +47,11 @@ def get_config(config=None): # retrieve common dictionary keys ipoe = get_accel_dict(conf, base, ipoe_chap_secrets) - if jmespath.search('client_ip_pool.name', ipoe): - dict_named_pools = jmespath.search('client_ip_pool.name', ipoe) + if dict_search('client_ip_pool', ipoe): # Multiple named pools require ordered values T5099 - ipoe['ordered_named_pools'] = get_pools_in_order(dict_named_pools) - # T5099 'next-pool' option - if jmespath.search('client_ip_pool.name.*.next_pool', ipoe): - for pool, pool_config in ipoe['client_ip_pool']['name'].items(): - if 'next_pool' in pool_config: - ipoe['first_named_pool'] = pool - ipoe['first_named_pool_subnet'] = pool_config - break + ipoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', ipoe)) + ipoe['server_type'] = 'ipoe' return ipoe @@ -156,9 +68,7 @@ def verify(ipoe): raise ConfigError('Option "client-subnet" incompatible with "vlan"!' 'Use "ipoe client-ip-pool" instead.') - #verify_accel_ppp_base_service(ipoe, local_users=False) - # IPoE server does not have 'gateway' option in the CLI - # we cannot use configverify.py verify_accel_ppp_base_service for ipoe-server + verify_accel_ppp_ip_pool(ipoe) if dict_search('authentication.mode', ipoe) == 'radius': if not dict_search('authentication.radius.server', ipoe): diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 87660c127..7c624f034 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -21,13 +21,16 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configdict import is_node_changed -from vyos.configverify import verify_accel_ppp_base_service from vyos.configverify import verify_interface_exists from vyos.template import render from vyos.utils.process import call from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag + airbag.enable() pppoe_conf = r'/run/accel-pppd/pppoe.conf' @@ -45,6 +48,10 @@ def get_config(config=None): # retrieve common dictionary keys pppoe = get_accel_dict(conf, base, pppoe_chap_secrets) + if dict_search('client_ip_pool', pppoe): + # Multiple named pools require ordered values T5099 + pppoe['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pppoe)) + # reload-or-restart does not implemented in accel-ppp # use this workaround until it will be implemented # https://phabricator.accel-ppp.org/T3 @@ -53,7 +60,7 @@ def get_config(config=None): is_node_changed(conf, base + ['interface'])] if any(conditions): pppoe.update({'restart_required': {}}) - + pppoe['server_type'] = 'pppoe' return pppoe def verify(pppoe): @@ -72,12 +79,7 @@ def verify(pppoe): for interface in pppoe['interface']: verify_interface_exists(interface) - # local ippool and gateway settings config checks - if not (dict_search('client_ip_pool.subnet', pppoe) or - (dict_search('client_ip_pool.name', pppoe) or - (dict_search('client_ip_pool.start', pppoe) and - dict_search('client_ip_pool.stop', pppoe)))): - print('Warning: No PPPoE client pool defined') + verify_accel_ppp_ip_pool(pppoe) if dict_search('authentication.radius.dynamic_author.server', pppoe): if not dict_search('authentication.radius.dynamic_author.key', pppoe): diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 6232ce64a..9a022d93c 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -21,15 +21,16 @@ from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit -from ipaddress import ip_network - from vyos.config import Config from vyos.template import is_ipv4 from vyos.template import render from vyos.utils.process import call from vyos.utils.system import get_half_cpus +from vyos.utils.dict import dict_search from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag @@ -43,7 +44,7 @@ default_config_data = { 'auth_ppp_mppe': 'prefer', 'auth_proto': ['auth_mschap_v2'], 'chap_secrets_file': l2tp_chap_secrets, # used in Jinja2 template - 'client_ip_pool': None, + 'client_ip_pool': {}, 'client_ip_subnets': [], 'client_ipv6_pool': [], 'client_ipv6_pool_configured': False, @@ -246,13 +247,14 @@ def get_config(config=None): conf.set_level(base_path) if conf.exists(['client-ip-pool']): - if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): - start = conf.return_value(['client-ip-pool', 'start']) - stop = conf.return_value(['client-ip-pool', 'stop']) - l2tp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + for pool_name in conf.list_nodes(['client-ip-pool']): + l2tp['client_ip_pool'][pool_name] = {} + l2tp['client_ip_pool'][pool_name]['range'] = conf.return_value(['client-ip-pool', pool_name, 'range']) + l2tp['client_ip_pool'][pool_name]['next_pool'] = conf.return_value(['client-ip-pool', pool_name, 'next-pool']) - if conf.exists(['client-ip-pool', 'subnet']): - l2tp['client_ip_subnets'] = conf.return_values(['client-ip-pool', 'subnet']) + if dict_search('client_ip_pool', l2tp): + # Multiple named pools require ordered values T5099 + l2tp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', l2tp)) if conf.exists(['client-ipv6-pool', 'prefix']): l2tp['client_ipv6_pool_configured'] = True @@ -281,23 +283,15 @@ def get_config(config=None): l2tp['client_ipv6_delegate_prefix'].append(tmp) + if conf.exists(['default-pool']): + l2tp['default_pool'] = conf.return_value(['default-pool']) + if conf.exists(['mtu']): l2tp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): l2tp['gateway_address'] = conf.return_value(['gateway-address']) - else: - # calculate gw-ip-address - if conf.exists(['client-ip-pool', 'start']): - # use start ip as gw-ip-address - l2tp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) - - elif conf.exists(['client-ip-pool', 'subnet']): - # use first ip address from first defined pool - subnet = conf.return_values(['client-ip-pool', 'subnet'])[0] - subnet = ip_network(subnet) - l2tp['gateway_address'] = str(list(subnet.hosts())[0]) # LNS secret if conf.exists(['lns', 'shared-secret']): @@ -330,9 +324,13 @@ def get_config(config=None): if conf.exists(['ppp-options', 'ipv6-peer-intf-id']): l2tp['ppp_ipv6_peer_intf_id'] = conf.return_value(['ppp-options', 'ipv6-peer-intf-id']) + l2tp['server_type'] = 'l2tp' return l2tp + + + def verify(l2tp): if not l2tp: return None @@ -366,10 +364,11 @@ def verify(l2tp): not is_listen_port_bind_service(int(port), 'accel-pppd'): raise ConfigError(f'"{proto}" port "{port}" is used by another service') - # check for the existence of a client ip pool - if not (l2tp['client_ip_pool'] or l2tp['client_ip_subnets']): - raise ConfigError( - "set vpn l2tp remote-access client-ip-pool requires subnet or start/stop IP pool") + if l2tp['auth_mode'] == 'local' or l2tp['auth_mode'] == 'noauth': + if not l2tp['client_ip_pool']: + raise ConfigError( + "L2TP local auth mode requires local client-ip-pool to be configured!") + verify_accel_ppp_ip_pool(l2tp) # check ipv6 if l2tp['client_ipv6_delegate_prefix'] and not l2tp['client_ipv6_pool']: diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index d542f57fe..6243c3ed3 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -21,10 +21,14 @@ from copy import deepcopy from stat import S_IRUSR, S_IWUSR, S_IRGRP from sys import exit + from vyos.config import Config from vyos.template import render from vyos.utils.system import get_half_cpus from vyos.utils.process import call +from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos import ConfigError from vyos import airbag @@ -54,7 +58,7 @@ default_pptp = { 'outside_addr': '', 'dnsv4': [], 'wins': [], - 'client_ip_pool': '', + 'client_ip_pool': {}, 'mtu': '1436', 'auth_proto' : ['auth_mschap_v2'], 'ppp_mppe' : 'prefer', @@ -205,22 +209,24 @@ def get_config(config=None): conf.set_level(base_path) if conf.exists(['client-ip-pool']): - if conf.exists(['client-ip-pool', 'start']) and conf.exists(['client-ip-pool', 'stop']): - start = conf.return_value(['client-ip-pool', 'start']) - stop = conf.return_value(['client-ip-pool', 'stop']) - pptp['client_ip_pool'] = start + '-' + re.search('[0-9]+$', stop).group(0) + for pool_name in conf.list_nodes(['client-ip-pool']): + pptp['client_ip_pool'][pool_name] = {} + pptp['client_ip_pool'][pool_name]['range'] = conf.return_value(['client-ip-pool', pool_name, 'range']) + pptp['client_ip_pool'][pool_name]['next_pool'] = conf.return_value(['client-ip-pool', pool_name, 'next-pool']) + + if dict_search('client_ip_pool', pptp): + # Multiple named pools require ordered values T5099 + pptp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', pptp)) + + if conf.exists(['default-pool']): + pptp['default_pool'] = conf.return_value(['default-pool']) if conf.exists(['mtu']): pptp['mtu'] = conf.return_value(['mtu']) # gateway address if conf.exists(['gateway-address']): - pptp['gw_ip'] = conf.return_value(['gateway-address']) - else: - # calculate gw-ip-address - if conf.exists(['client-ip-pool', 'start']): - # use start ip as gw-ip-address - pptp['gateway_address'] = conf.return_value(['client-ip-pool', 'start']) + pptp['gateway_address'] = conf.return_value(['gateway-address']) if conf.exists(['authentication', 'require']): # clear default list content, now populate with actual CLI values @@ -238,6 +244,7 @@ def get_config(config=None): if conf.exists(['authentication', 'mppe']): pptp['ppp_mppe'] = conf.return_value(['authentication', 'mppe']) + pptp['server_type'] = 'pptp' return pptp @@ -248,21 +255,25 @@ def verify(pptp): if pptp['auth_mode'] == 'local': if not pptp['local_users']: raise ConfigError('PPTP local auth mode requires local users to be configured!') - for user in pptp['local_users']: username = user['name'] if not user['password']: raise ConfigError(f'Password required for local user "{username}"') - elif pptp['auth_mode'] == 'radius': if len(pptp['radius_server']) == 0: raise ConfigError('RADIUS authentication requires at least one server') - for radius in pptp['radius_server']: if not radius['key']: server = radius['server'] raise ConfigError(f'Missing RADIUS secret key for server "{ server }"') + if pptp['auth_mode'] == 'local' or pptp['auth_mode'] == 'noauth': + if not pptp['client_ip_pool']: + raise ConfigError( + "PPTP local auth mode requires local client-ip-pool to be configured!") + + verify_accel_ppp_ip_pool(pptp) + if len(pptp['dnsv4']) > 2: raise ConfigError('Not more then two IPv4 DNS name-servers can be configured') diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index e98d8385b..ac053cc76 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.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 @@ -21,13 +21,15 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_accel_dict from vyos.configdict import dict_merge -from vyos.configverify import verify_accel_ppp_base_service from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key from vyos.template import render from vyos.utils.process import call from vyos.utils.network import check_port_availability from vyos.utils.dict import dict_search +from vyos.accel_ppp_util import verify_accel_ppp_base_service +from vyos.accel_ppp_util import verify_accel_ppp_ip_pool +from vyos.accel_ppp_util import get_pools_in_order from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file from vyos import ConfigError @@ -53,13 +55,17 @@ def get_config(config=None): # retrieve common dictionary keys sstp = get_accel_dict(conf, base, sstp_chap_secrets) + if dict_search('client_ip_pool', sstp): + # Multiple named pools require ordered values T5099 + sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp)) if sstp: sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) - + sstp['server_type'] = 'sstp' return sstp + def verify(sstp): if not sstp: return None @@ -75,6 +81,7 @@ def verify(sstp): if 'client_ip_pool' not in sstp and 'client_ipv6_pool' not in sstp: raise ConfigError('Client IP subnet required') + verify_accel_ppp_ip_pool(sstp) # # SSL certificate checks # diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 9eb6fac48..441b316c2 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-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 @@ -13,8 +13,9 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -## Script called up strongswan to bring the vti interface up/down based on the state of the IPSec tunnel. -## Called as vti_up_down vti_intf_name + +# Script called up strongswan to bring the VTI interface up/down based on +# the state of the IPSec tunnel. Called as vti_up_down vti_intf_name import os import sys @@ -25,9 +26,10 @@ from syslog import LOG_PID from syslog import LOG_INFO from vyos.configquery import ConfigTreeQuery +from vyos.configdict import get_interface_dict +from vyos.ifconfig import VTIIf from vyos.utils.process import call from vyos.utils.network import get_interface_config -from vyos.utils.network import get_interface_address if __name__ == '__main__': verb = os.getenv('PLUTO_VERB') @@ -48,14 +50,13 @@ if __name__ == '__main__': vti_link_up = (vti_link['operstate'] != 'DOWN' if 'operstate' in vti_link else False) - config = ConfigTreeQuery() - vti_dict = config.get_config_dict(['interfaces', 'vti', interface], - get_first_key=True) - if verb in ['up-client', 'up-host']: if not vti_link_up: - if 'disable' not in vti_dict: - call(f'sudo ip link set {interface} up') + conf = ConfigTreeQuery() + _, vti = get_interface_dict(conf.config, ['interfaces', 'vti'], interface) + if 'disable' not in vti: + tmp = VTIIf(interface) + tmp.update(vti) else: syslog(f'Interface {interface} is admin down ...') elif verb in ['down-client', 'down-host']: diff --git a/src/migration-scripts/conntrack/4-to-5 b/src/migration-scripts/conntrack/4-to-5 new file mode 100755 index 000000000..d2e5fc5fa --- /dev/null +++ b/src/migration-scripts/conntrack/4-to-5 @@ -0,0 +1,59 @@ +#!/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/>. + +# T5779: system conntrack timeout custom +# Before: +# Protocols tcp, udp and icmp allowed. When using udp it did not work +# Only ipv4 custom timeout rules +# Now: +# Valid protocols are only tcp or udp. +# Extend functionality to ipv6 and move ipv4 custom rules to new node: +# set system conntrack timeout custom [ipv4 | ipv6] rule <rule> ... + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['system', 'conntrack'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['timeout', 'custom', 'rule']): + for rule in config.list_nodes(base + ['timeout', 'custom', 'rule']): + if config.exists(base + ['timeout', 'custom', 'rule', rule, 'protocol', 'tcp']): + config.set(base + ['timeout', 'custom', 'ipv4', 'rule']) + config.copy(base + ['timeout', 'custom', 'rule', rule], base + ['timeout', 'custom', 'ipv4', 'rule', rule]) + config.delete(base + ['timeout', 'custom', 'rule']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/dns-dynamic/2-to-3 b/src/migration-scripts/dns-dynamic/2-to-3 new file mode 100755 index 000000000..187c2a895 --- /dev/null +++ b/src/migration-scripts/dns-dynamic/2-to-3 @@ -0,0 +1,88 @@ +#!/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/>. + +# T5791: +# - migrate "service dns dynamic address web web-options ..." +# to "service dns dynamic name <service> address web ..." (per service) +# - migrate "service dns dynamic address <address> rfc2136 <service> ..." +# to "service dns dynamic name <service> address <interface> protocol 'nsupdate'" +# - migrate "service dns dynamic address <interface> service <service> ..." +# to "service dns dynamic name <service> address <interface> ..." + +import sys +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base_path = ['service', 'dns', 'dynamic'] +address_path = base_path + ['address'] +name_path = base_path + ['name'] + +if not config.exists(address_path): + # Nothing to do + sys.exit(0) + +# config.copy does not recursively create a path, so initialize the name path as tagged node +if not config.exists(name_path): + config.set(name_path) + config.set_tag(name_path) + +for address in config.list_nodes(address_path): + + address_path_tag = address_path + [address] + + # Move web-option as a configuration in each service instead of top level web-option + if config.exists(address_path_tag + ['web-options']) and address == 'web': + for svc_type in ['service', 'rfc2136']: + if config.exists(address_path_tag + [svc_type]): + for svc_cfg in config.list_nodes(address_path_tag + [svc_type]): + config.copy(address_path_tag + ['web-options'], + address_path_tag + [svc_type, svc_cfg, 'web-options']) + config.delete(address_path_tag + ['web-options']) + + for svc_type in ['service', 'rfc2136']: + if config.exists(address_path_tag + [svc_type]): + # Move RFC2136 as service configuration, rename to avoid name conflict and set protocol to 'nsupdate' + if svc_type == 'rfc2136': + for rfc_cfg_old in config.list_nodes(address_path_tag + ['rfc2136']): + rfc_cfg_new = f'{rfc_cfg_old}-rfc2136' + config.rename(address_path_tag + ['rfc2136', rfc_cfg_old], rfc_cfg_new) + config.set(address_path_tag + ['rfc2136', rfc_cfg_new, 'protocol'], 'nsupdate') + + # Add address as config value in each service before moving the service path + # And then copy the services from 'address <interface> service <service>' to 'name <service>' + for svc_cfg in config.list_nodes(address_path_tag + [svc_type]): + config.set(address_path_tag + [svc_type, svc_cfg, 'address'], address) + config.copy(address_path_tag + [svc_type, svc_cfg], name_path + [svc_cfg]) + +# Finally cleanup the old address path +config.delete(address_path) + +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/ipoe-server/1-to-2 b/src/migration-scripts/ipoe-server/1-to-2 new file mode 100755 index 000000000..c8cec6835 --- /dev/null +++ b/src/migration-scripts/ipoe-server/1-to-2 @@ -0,0 +1,87 @@ +#!/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/>. + +# - changed cli of all named pools +# - moved gateway-address from pool to global configuration with / netmask +# gateway can exist without pool if radius is used +# and Framed-ip-address is transmited +# - There are several gateway-addresses in ipoe +# - default-pool by migration. +# 1. The first pool that contains next-poll. +# 2. Else, the first pool in the list + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'ipoe-server'] +pool_base = base + ['client-ip-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) +default_pool = '' +gateway = '' + +#named pool migration +namedpools_base = pool_base + ['name'] + +for pool_name in config.list_nodes(namedpools_base): + pool_path = namedpools_base + [pool_name] + if config.exists(pool_path + ['subnet']): + subnet = config.return_value(pool_path + ['subnet']) + config.set(pool_base + [pool_name, 'range'], value=subnet) + # Get netmask from subnet + mask = subnet.split("/")[1] + if config.exists(pool_path + ['next-pool']): + next_pool = config.return_value(pool_path + ['next-pool']) + config.set(pool_base + [pool_name, 'next-pool'], value=next_pool) + if not default_pool: + default_pool = pool_name + if config.exists(pool_path + ['gateway-address']) and mask: + gateway = f'{config.return_value(pool_path + ["gateway-address"])}/{mask}' + config.set(base + ['gateway-address'], value=gateway, replace=False) + +if not default_pool and config.list_nodes(namedpools_base): + default_pool = config.list_nodes(namedpools_base)[0] + +config.delete(namedpools_base) + +if default_pool: + config.set(base + ['default-pool'], value=default_pool) +# format as tag node +config.set_tag(pool_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/l2tp/4-to-5 b/src/migration-scripts/l2tp/4-to-5 new file mode 100755 index 000000000..fe8ab357e --- /dev/null +++ b/src/migration-scripts/l2tp/4-to-5 @@ -0,0 +1,77 @@ +#!/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/>. + +# - move all pool to named pools +# 'start-stop' migrate to namedpool 'default-range-pool' +# 'subnet' migrate to namedpool 'default-subnet-pool' +# 'default-subnet-pool' is the next pool for 'default-range-pool' + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'l2tp', 'remote-access'] +pool_base = base + ['client-ip-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) +default_pool = '' +range_pool_name = 'default-range-pool' +subnet_pool_name = 'default-subnet-pool' +if config.exists(pool_base + ['subnet']): + subnet = config.return_value(pool_base + ['subnet']) + config.delete(pool_base + ['subnet']) + config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) + default_pool = subnet_pool_name + +if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + start_ip = config.return_value(pool_base + ['start']) + stop_ip = config.return_value(pool_base + ['stop']) + ip_range = f'{start_ip}-{stop_ip}' + config.delete(pool_base + ['start']) + config.delete(pool_base + ['stop']) + config.set(pool_base + [range_pool_name, 'range'], value=ip_range) + if default_pool: + config.set(pool_base + [range_pool_name, 'next-pool'], + value=subnet_pool_name) + default_pool = range_pool_name + +if default_pool: + config.set(base + ['default-pool'], value=default_pool) +# format as tag node +config.set_tag(pool_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/pppoe-server/6-to-7 b/src/migration-scripts/pppoe-server/6-to-7 new file mode 100755 index 000000000..34996d8fe --- /dev/null +++ b/src/migration-scripts/pppoe-server/6-to-7 @@ -0,0 +1,111 @@ +#!/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/>. + +# - move all pool to named pools +# 'start-stop' migrate to namedpool 'default-range-pool' +# 'subnet' migrate to namedpool 'default-subnet-pool' +# 'default-subnet-pool' is the next pool for 'default-range-pool' +# - There is only one gateway-address, take the first which is configured +# - default-pool by migration. +# 1. If authentication mode = 'local' then it is first named pool. +# If there are not named pools, namedless pool will be default. +# 2. If authentication mode = 'radius' then namedless pool will be default + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['service', 'pppoe-server'] +pool_base = base + ['client-ip-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) +default_pool = '' +range_pool_name = 'default-range-pool' +subnet_pool_name = 'default-subnet-pool' +#Default nameless pools migrations +if config.exists(pool_base + ['subnet']): + subnet = config.return_value(pool_base + ['subnet']) + config.delete(pool_base + ['subnet']) + config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) + default_pool = subnet_pool_name + +if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + start_ip = config.return_value(pool_base + ['start']) + stop_ip = config.return_value(pool_base + ['stop']) + ip_range = f'{start_ip}-{stop_ip}' + config.delete(pool_base + ['start']) + config.delete(pool_base + ['stop']) + config.set(pool_base + [range_pool_name, 'range'], value=ip_range) + if default_pool: + config.set(pool_base + [range_pool_name, 'next-pool'], + value=subnet_pool_name) + default_pool = range_pool_name + +gateway = '' +if config.exists(base + ['gateway-address']): + gateway = config.return_value(base + ['gateway-address']) + +#named pool migration +namedpools_base = pool_base + ['name'] +if config.exists(namedpools_base): + if config.exists(base + ['authentication', 'mode']): + if config.return_value(base + ['authentication', 'mode']) == 'local': + if config.list_nodes(namedpools_base): + default_pool = config.list_nodes(namedpools_base)[0] + + for pool_name in config.list_nodes(namedpools_base): + pool_path = namedpools_base + [pool_name] + if config.exists(pool_path + ['subnet']): + subnet = config.return_value(pool_path + ['subnet']) + config.set(pool_base + [pool_name, 'range'], value=subnet) + if config.exists(pool_path + ['next-pool']): + next_pool = config.return_value(pool_path + ['next-pool']) + config.set(pool_base + [pool_name, 'next-pool'], value=next_pool) + if not gateway: + if config.exists(pool_path + ['gateway-address']): + gateway = config.return_value(pool_path + ['gateway-address']) + + config.delete(namedpools_base) + +if gateway: + config.set(base + ['gateway-address'], value=gateway) +if default_pool: + config.set(base + ['default-pool'], value=default_pool) +# format as tag node +config.set_tag(pool_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/pptp/2-to-3 b/src/migration-scripts/pptp/2-to-3 new file mode 100755 index 000000000..98dc5c2a6 --- /dev/null +++ b/src/migration-scripts/pptp/2-to-3 @@ -0,0 +1,64 @@ +#!/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/>. + +# - move all pool to named pools +# 'start-stop' migrate to namedpool 'default-range-pool' +# 'default-subnet-pool' is the next pool for 'default-range-pool' + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'pptp', 'remote-access'] +pool_base = base + ['client-ip-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +range_pool_name = 'default-range-pool' + +if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + start_ip = config.return_value(pool_base + ['start']) + stop_ip = config.return_value(pool_base + ['stop']) + ip_range = f'{start_ip}-{stop_ip}' + config.delete(pool_base + ['start']) + config.delete(pool_base + ['stop']) + config.set(pool_base + [range_pool_name, 'range'], value=ip_range) + config.set(base + ['default-pool'], value=range_pool_name) +# format as tag node +config.set_tag(pool_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/sstp/4-to-5 b/src/migration-scripts/sstp/4-to-5 new file mode 100755 index 000000000..0f332e04f --- /dev/null +++ b/src/migration-scripts/sstp/4-to-5 @@ -0,0 +1,60 @@ +#!/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/>. + +# - move all pool to named pools +# 'subnet' migrate to namedpool 'default-subnet-pool' +# 'default-subnet-pool' is the next pool for 'default-range-pool' + +import os + +from sys import argv +from sys import exit +from vyos.configtree import ConfigTree + + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) +base = ['vpn', 'sstp'] +pool_base = base + ['client-ip-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +subnet_pool_name = 'default-subnet-pool' +if config.exists(pool_base + ['subnet']): + subnet = config.return_value(pool_base + ['subnet']) + config.delete(pool_base + ['subnet']) + config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) + config.set(base + ['default-pool'], value=subnet_pool_name) +# format as tag node +config.set_tag(pool_base) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index aa4cf301b..b3e6e518c 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -20,6 +20,7 @@ from argparse import ArgumentParser, Namespace from pathlib import Path from shutil import copy, chown, rmtree, copytree +from glob import glob from sys import exit from time import sleep from typing import Union @@ -59,6 +60,8 @@ MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user' MSG_INPUT_ROOT_SIZE_ALL: str = 'Would you like to use all the free space on the drive?' MSG_INPUT_ROOT_SIZE_SET: str = 'Please specify the size (in GB) of the root partition (min is 1.5 GB)?' MSG_INPUT_CONSOLE_TYPE: str = 'What console should be used by default? (K: KVM, S: Serial, U: USB-Serial)?' +MSG_INPUT_COPY_DATA: str = 'Would you like to copy data to the new image?' +MSG_INPUT_CHOOSE_COPY_DATA: str = 'From which image would you like to save config information?' MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continue with installation?' MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?' MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.' @@ -183,6 +186,83 @@ def create_partitions(target_disk: str, target_size: int, return disk_details +def search_format_selection(image: tuple[str, str]) -> str: + """Format a string for selection of image + + Args: + image (tuple[str, str]): a tuple of image name and drive + + Returns: + str: formatted string + """ + return f'{image[0]} on {image[1]}' + + +def search_previous_installation(disks: list[str]) -> None: + """Search disks for previous installation config and SSH keys + + Args: + disks (list[str]): a list of available disks + """ + mnt_config = '/mnt/config' + mnt_ssh = '/mnt/ssh' + mnt_tmp = '/mnt/tmp' + rmtree(Path(mnt_config), ignore_errors=True) + rmtree(Path(mnt_ssh), ignore_errors=True) + Path(mnt_tmp).mkdir(exist_ok=True) + + print('Searching for data from previous installations') + image_data = [] + for disk_name in disks: + for partition in disk.partition_list(disk_name): + if disk.partition_mount(partition, mnt_tmp): + if Path(mnt_tmp + '/boot').exists(): + for path in Path(mnt_tmp + '/boot').iterdir(): + if path.joinpath('rw/config/.vyatta_config').exists(): + image_data.append((path.name, partition)) + + disk.partition_umount(partition) + + if len(image_data) == 1: + image_name, image_drive = image_data[0] + print('Found data from previous installation:') + print(f'\t{image_name} on {image_drive}') + if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True): + return + + elif len(image_data) > 1: + print('Found data from previous installations') + if not ask_yes_no(MSG_INPUT_COPY_DATA, default=True): + return + + image_name, image_drive = select_entry(image_data, + 'Available versions:', + MSG_INPUT_CHOOSE_COPY_DATA, + search_format_selection) + else: + print('No previous installation found') + return + + disk.partition_mount(image_drive, mnt_tmp) + + copytree(f'{mnt_tmp}/boot/{image_name}/rw/config', mnt_config) + Path(mnt_ssh).mkdir() + host_keys: list[str] = glob(f'{mnt_tmp}/boot/{image_name}/rw/etc/ssh/ssh_host*') + for host_key in host_keys: + copy(host_key, mnt_ssh) + + disk.partition_umount(image_drive) + + +def copy_previous_installation_data(target_dir: str) -> None: + if Path('/mnt/config').exists(): + copytree('/mnt/config', f'{target_dir}/opt/vyatta/etc/config', + dirs_exist_ok=True) + if Path('/mnt/ssh').exists(): + copytree('/mnt/ssh', f'{target_dir}/etc/ssh', + dirs_exist_ok=True) + + def ask_single_disk(disks_available: dict[str, int]) -> str: """Ask user to select a disk for installation @@ -203,6 +283,8 @@ def ask_single_disk(disks_available: dict[str, int]) -> str: print(MSG_INFO_INSTALL_EXIT) exit() + search_previous_installation(list(disks_available)) + disk_details: disk.DiskDetails = create_partitions(disk_selected, disks_available[disk_selected]) @@ -259,6 +341,8 @@ def check_raid_install(disks_available: dict[str, int]) -> Union[str, None]: print(MSG_INFO_INSTALL_EXIT) exit() + search_previous_installation(list(disks_available)) + disks: list[disk.DiskDetails] = [] for disk_selected in list(disks_selected): print(f'Creating partitions on {disk_selected}') @@ -376,7 +460,7 @@ def validate_signature(file_path: str, sign_type: str) -> None: print('Signature is valid') -def image_fetch(image_path: str) -> Path: +def image_fetch(image_path: str, no_prompt: bool = False) -> Path: """Fetch an ISO image Args: @@ -389,13 +473,14 @@ def image_fetch(image_path: str) -> Path: # check a type of path if urlparse(image_path).scheme: # download an image - download(ISO_DOWNLOAD_PATH, image_path, True, True) + download(ISO_DOWNLOAD_PATH, image_path, True, True, + raise_error=True) # download a signature sign_file = (False, '') for sign_type in ['minisig', 'asc']: try: download(f'{ISO_DOWNLOAD_PATH}.{sign_type}', - f'{image_path}.{sign_type}') + f'{image_path}.{sign_type}', raise_error=True) sign_file = (True, sign_type) break except Exception: @@ -404,7 +489,8 @@ def image_fetch(image_path: str) -> Path: if sign_file[0]: validate_signature(ISO_DOWNLOAD_PATH, sign_file[1]) else: - if not ask_yes_no(MSG_WARN_ISO_SIGN_UNAVAL, default=False): + if (not no_prompt and + not ask_yes_no(MSG_WARN_ISO_SIGN_UNAVAL, default=False)): cleanup() exit(MSG_INFO_INSTALL_EXIT) @@ -433,6 +519,17 @@ def migrate_config() -> bool: return False +def copy_ssh_host_keys() -> bool: + """Ask user to copy SSH host keys + + Returns: + bool: user's decision + """ + if ask_yes_no('Would you like to copy SSH host keys?', default=True): + return True + return False + + def cleanup(mounts: list[str] = [], remove_items: list[str] = []) -> None: """Clean up after installation @@ -567,6 +664,10 @@ def install_image() -> None: copy(FILE_ROOTFS_SRC, f'{DIR_DST_ROOT}/boot/{image_name}/{image_name}.squashfs') + # copy saved config data and SSH keys + # owner restored on copy of config data by chmod_2775, above + copy_previous_installation_data(f'{DIR_DST_ROOT}/boot/{image_name}/rw') + if is_raid_install(install_target): write_dir: str = f'{DIR_DST_ROOT}/boot/{image_name}/rw' raid.update_default(write_dir) @@ -629,7 +730,7 @@ def install_image() -> None: @compat.grub_cfg_update -def add_image(image_path: str) -> None: +def add_image(image_path: str, no_prompt: bool = False) -> None: """Add a new image Args: @@ -639,7 +740,7 @@ def add_image(image_path: str) -> None: exit(MSG_ERR_LIVE) # fetch an image - iso_path: Path = image_fetch(image_path) + iso_path: Path = image_fetch(image_path, no_prompt) try: # mount an ISO Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True) @@ -668,8 +769,12 @@ def add_image(image_path: str) -> None: raise compat.DowngradingImageTools( f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed') - image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) - set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True) + if not no_prompt: + image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) + set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True) + else: + image_name: str = version_name + set_as_default: bool = True # find target directory root_dir: str = disk.find_persistence() @@ -678,7 +783,7 @@ def add_image(image_path: str) -> None: # create all the rest in a single step target_config_dir: str = f'{root_dir}/boot/{image_name}/rw/opt/vyatta/etc/config/' # copy config - if migrate_config(): + if no_prompt or migrate_config(): print('Copying configuration directory') # copytree preserves perms but not ownership: Path(target_config_dir).mkdir(parents=True) @@ -692,6 +797,14 @@ def add_image(image_path: str) -> None: chmod_2775(target_config_dir) Path(f'{target_config_dir}/.vyatta_config').touch() + target_ssh_dir: str = f'{root_dir}/boot/{image_name}/rw/etc/ssh/' + if no_prompt or copy_ssh_host_keys(): + print('Copying SSH host keys') + Path(target_ssh_dir).mkdir(parents=True) + host_keys: list[str] = glob('/etc/ssh/ssh_host*') + for host_key in host_keys: + copy(host_key, target_ssh_dir) + # copy system image and kernel files print('Copying system image files') for file in Path(f'{DIR_ISO_MOUNT}/live').iterdir(): @@ -727,8 +840,10 @@ def parse_arguments() -> Namespace: choices=['install', 'add'], required=True, help='action to perform with an image') + parser.add_argument('--no-prompt', action='store_true', + help='perform action non-interactively') parser.add_argument( - '--image_path', + '--image-path', help='a path (HTTP or local file) to an image that needs to be installed' ) # parser.add_argument('--image_new_name', help='a new name for image') @@ -746,7 +861,7 @@ if __name__ == '__main__': if args.action == 'install': install_image() if args.action == 'add': - add_image(args.image_path) + add_image(args.image_path, args.no_prompt) exit() diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index e4b2f4833..e75485f9f 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -36,7 +36,7 @@ MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another im @compat.grub_cfg_update def delete_image(image_name: Optional[str] = None, - prompt: bool = True) -> None: + no_prompt: bool = False) -> None: """Remove installed image files and boot entry Args: @@ -44,7 +44,7 @@ def delete_image(image_name: Optional[str] = None, """ available_images: list[str] = grub.version_list() if image_name is None: - if not prompt: + if no_prompt: exit('An image name is required for delete action') else: image_name = select_entry(available_images, @@ -60,8 +60,9 @@ def delete_image(image_name: Optional[str] = None, if not persistence_storage: exit('Persistence storage cannot be found') - if not ask_yes_no(f'Do you really want to delete the image {image_name}?', - default=False): + if (not no_prompt and + not ask_yes_no(f'Do you really want to delete the image {image_name}?', + default=False)): exit() # remove files and menu entry @@ -171,11 +172,13 @@ def parse_arguments() -> Namespace: choices=['delete', 'set', 'rename', 'list'], required=True, help='action to perform with an image') + parser.add_argument('--no-prompt', action='store_true', + help='perform action non-interactively') parser.add_argument( - '--image_name', + '--image-name', help= 'a name of an image to add, delete, install, rename, or set as default') - parser.add_argument('--image_new_name', help='a new name for image') + parser.add_argument('--image-new-name', help='a new name for image') args: Namespace = parser.parse_args() # Validate arguments if args.action == 'rename' and (not args.image_name or @@ -189,7 +192,7 @@ if __name__ == '__main__': try: args: Namespace = parse_arguments() if args.action == 'delete': - delete_image(args.image_name) + delete_image(args.image_name, args.no_prompt) if args.action == 'set': set_image(args.image_name) if args.action == 'rename': diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index 8a90786e2..bfd50cc80 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -50,7 +50,7 @@ from vyos.configsession import ConfigSession, ConfigSessionError import api.graphql.state -api_config_state = '/tmp/api-config-state' +api_config_state = '/run/http-api-state' CFG_GROUP = 'vyattacfg' debug = True diff --git a/src/system/grub_update.py b/src/system/grub_update.py index 366a85344..3c851f0e0 100644 --- a/src/system/grub_update.py +++ b/src/system/grub_update.py @@ -41,6 +41,9 @@ if __name__ == '__main__': if image.is_live_boot(): exit(0) + if image.is_running_as_container(): + exit(0) + # Skip everything if update is not required if not cfg_check_update(): exit(0) diff --git a/src/validators/ddclient-protocol b/src/validators/ddclient-protocol index 8f455e12e..ce5efbd52 100755 --- a/src/validators/ddclient-protocol +++ b/src/validators/ddclient-protocol @@ -14,7 +14,7 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -ddclient -list-protocols | grep -vE 'nsupdate|cloudns|porkbun' | grep -qw $1 +ddclient -list-protocols | grep -vE 'cloudns|porkbun' | grep -qw $1 if [ $? -gt 0 ]; then echo "Error: $1 is not a valid protocol, please choose from the supported list of protocols" diff --git a/src/validators/ipv4-range-mask b/src/validators/ipv4-range-mask new file mode 100755 index 000000000..7bb4539af --- /dev/null +++ b/src/validators/ipv4-range-mask @@ -0,0 +1,59 @@ +#!/bin/bash + +# snippet from https://stackoverflow.com/questions/10768160/ip-address-converter +ip2dec () { + local a b c d ip=$@ + IFS=. read -r a b c d <<< "$ip" + printf '%d\n' "$((a * 256 ** 3 + b * 256 ** 2 + c * 256 + d))" +} + +error_exit() { + echo "Error: $1 is not a valid IPv4 address range or these IPs are not under /$2" + exit 1 +} + +# Check if address range is under the same netmask +# -m - mask +# -r - IP range in format x.x.x.x-y.y.y.y +while getopts m:r: flag +do + case "${flag}" in + m) mask=${OPTARG};; + r) range=${OPTARG} + esac +done +if [[ "${range}" =~ "-" ]]&&[[ ! -z ${mask} ]]; then + # This only works with real bash (<<<) - split IP addresses into array with + # hyphen as delimiter + readarray -d - -t strarr <<< ${range} + + ipaddrcheck --is-ipv4-single ${strarr[0]} + if [ $? -gt 0 ]; then + error_exit ${range} ${mask} + fi + + ipaddrcheck --is-ipv4-single ${strarr[1]} + if [ $? -gt 0 ]; then + error_exit ${range} ${mask} + fi + + ${vyos_validators_dir}/numeric --range 0-32 ${mask} > /dev/null + if [ $? -ne 0 ]; then + error_exit ${range} ${mask} + fi + + is_in_24=$( grepcidr ${strarr[0]}"/"${mask} <(echo ${strarr[1]}) ) + if [ -z $is_in_24 ]; then + error_exit ${range} ${mask} + fi + + start=$(ip2dec ${strarr[0]}) + stop=$(ip2dec ${strarr[1]}) + if [ $start -ge $stop ]; then + error_exit ${range} ${mask} + fi + + exit 0 +fi + +error_exit ${range} ${mask} |