diff options
60 files changed, 2150 insertions, 995 deletions
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/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/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/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/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/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/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/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/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/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/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_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_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_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_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/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/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/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/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 df5d897b7..cdb84a152 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 @@ -435,6 +436,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 @@ -698,6 +710,14 @@ def add_image(image_path: str, no_prompt: bool = False) -> 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(): 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} |