diff options
221 files changed, 4904 insertions, 2188 deletions
diff --git a/.github/workflows/pull-request-labels.yml b/.github/workflows/pull-request-labels.yml index 778daae30..3398af5b0 100644 --- a/.github/workflows/pull-request-labels.yml +++ b/.github/workflows/pull-request-labels.yml @@ -17,4 +17,4 @@ jobs: contents: read pull-requests: write steps: - - uses: actions/labeler@v5.0.0-alpha.1 + - uses: actions/labeler@v5.0.0 diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index ed9bb6cad..d3685caaf 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -9,21 +9,22 @@ "dhcp.py", "dns.py", "interfaces.py", +"ipsec.py", "lldp.py", "log.py", "memory.py", +"multicast.py", "nat.py", "neighbor.py", "nhrp.py", "openconnect.py", -"otp.py", "openvpn.py", +"otp.py", "reset_vpn.py", "reverseproxy.py", "route.py", -"system.py", -"ipsec.py", "storage.py", +"system.py", "uptime.py", "version.py", "vrf.py" diff --git a/data/templates/accel-ppp/config_ip_pool.j2 b/data/templates/accel-ppp/config_ip_pool.j2 index 6ac04e1a1..8e66486e6 100644 --- a/data/templates/accel-ppp/config_ip_pool.j2 +++ b/data/templates/accel-ppp/config_ip_pool.j2 @@ -12,16 +12,20 @@ gw-ip-address={{ gateway_address }} {% endif %} {% for pool in ordered_named_pools %} {% for pool_name, pool_config in pool.items() %} -{% set iprange_str = pool_config.range %} -{% set iprange_list = pool_config.range.split('-') %} -{% if iprange_list | length == 2 %} -{% set last_ip_oct = iprange_list[1].split('.') %} -{% set iprange_str = iprange_list[0] + '-' + last_ip_oct[last_ip_oct | length - 1] %} -{% endif %} -{% if pool_config.next_pool is vyos_defined %} +{% if pool_config.range is vyos_defined %} +{% for range in pool_config.range %} +{% set iprange_str = range %} +{% set iprange_list = range.split('-') %} +{% if iprange_list | length == 2 %} +{% set last_ip_oct = iprange_list[1].split('.') %} +{% set iprange_str = iprange_list[0] + '-' + last_ip_oct[last_ip_oct | length - 1] %} +{% endif %} +{% if loop.last and pool_config.next_pool is vyos_defined %} {{ iprange_str }},name={{ pool_name }},next={{ pool_config.next_pool }} -{% else %} +{% else %} {{ iprange_str }},name={{ pool_name }} +{% endif %} +{% endfor %} {% endif %} {% endfor %} {% endfor %} diff --git a/data/templates/accel-ppp/config_ipv6_pool.j2 b/data/templates/accel-ppp/config_ipv6_pool.j2 index a1562a1eb..86efdc1e1 100644 --- a/data/templates/accel-ppp/config_ipv6_pool.j2 +++ b/data/templates/accel-ppp/config_ipv6_pool.j2 @@ -3,20 +3,19 @@ AdvAutonomousFlag=1 verbose=1 -{% if client_ipv6_pool.prefix is vyos_defined %} [ipv6-pool] -{% for prefix, options in client_ipv6_pool.prefix.items() %} -{{ prefix }},{{ options.mask }} -{% endfor %} -{% if client_ipv6_pool.delegate is vyos_defined %} -{% for prefix, options in client_ipv6_pool.delegate.items() %} -delegate={{ prefix }},{{ options.delegation_prefix }} +{% for pool_name, pool_config in client_ipv6_pool.items() %} +{% if pool_config.prefix is vyos_defined %} +{% for prefix, options in pool_config.prefix.items() %} +{{ prefix }},{{ options.mask }},name={{ pool_name }} {% endfor %} {% endif %} -{% endif %} - -{% if client_ipv6_pool.delegate is vyos_defined %} +{% if pool_config.delegate is vyos_defined %} +{% for prefix, options in pool_config.delegate.items() %} +delegate={{ prefix }},{{ options.delegation_prefix }},name={{ pool_name }} +{% endfor %} +{% endif %} +{% endfor %} [ipv6-dhcp] verbose=1 -{% endif %} {% endif %} diff --git a/data/templates/accel-ppp/ipoe.config.j2 b/data/templates/accel-ppp/ipoe.config.j2 index 588f3d462..8b022eaa5 100644 --- a/data/templates/accel-ppp/ipoe.config.j2 +++ b/data/templates/accel-ppp/ipoe.config.j2 @@ -58,6 +58,10 @@ password=csid {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} +{% if default_ipv6_pool is vyos_defined %} +ipv6-pool={{ default_ipv6_pool }} +ipv6-pool-delegate={{ default_ipv6_pool }} +{% endif %} {% if gateway_address is vyos_defined %} {% for gw_addr in gateway_address %} gw-ip-address={{ gw_addr }} diff --git a/data/templates/accel-ppp/l2tp.config.j2 b/data/templates/accel-ppp/l2tp.config.j2 index 49755254a..f041e278e 100644 --- a/data/templates/accel-ppp/l2tp.config.j2 +++ b/data/templates/accel-ppp/l2tp.config.j2 @@ -51,6 +51,10 @@ host-name={{ lns.host_name }} {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} +{% if default_ipv6_pool is vyos_defined %} +ipv6-pool={{ default_ipv6_pool }} +ipv6-pool-delegate={{ default_ipv6_pool }} +{% endif %} [client-ip-range] 0.0.0.0/0 diff --git a/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2 index 4bb1c4450..fb8a11366 100644 --- a/data/templates/accel-ppp/pppoe.config.j2 +++ b/data/templates/accel-ppp/pppoe.config.j2 @@ -143,6 +143,10 @@ noauth=1 {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} +{% if default_ipv6_pool is vyos_defined %} +ipv6-pool={{ default_ipv6_pool }} +ipv6-pool-delegate={{ default_ipv6_pool }} +{% endif %} {% if limits is vyos_defined %} [connlimit] diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2 index 014ae1235..51f7dfca8 100644 --- a/data/templates/accel-ppp/sstp.config.j2 +++ b/data/templates/accel-ppp/sstp.config.j2 @@ -39,6 +39,10 @@ ssl-keyfile=/run/accel-pppd/sstp-cert.key {% if default_pool is vyos_defined %} ip-pool={{ default_pool }} {% endif %} +{% if default_ipv6_pool is vyos_defined %} +ipv6-pool={{ default_ipv6_pool }} +ipv6-pool-delegate={{ default_ipv6_pool }} +{% endif %} {# Common IP pool definitions #} {% include 'accel-ppp/config_ip_pool.j2' %} diff --git a/data/templates/chrony/chrony.conf.j2 b/data/templates/chrony/chrony.conf.j2 index d02fbf71d..e3f078fdc 100644 --- a/data/templates/chrony/chrony.conf.j2 +++ b/data/templates/chrony/chrony.conf.j2 @@ -21,7 +21,17 @@ ntsdumpdir /run/chrony pidfile {{ config_file | replace('.conf', '.pid') }} # Determine when will the next leap second occur and what is the current offset +{% if leap_second is vyos_defined('timezone') %} leapsectz right/UTC +{% elif leap_second is vyos_defined('ignore') %} +leapsecmode ignore +{% elif leap_second is vyos_defined('smear') %} +leapsecmode slew +maxslewrate 1000 +smoothtime 400 0.001024 leaponly +{% elif leap_second is vyos_defined('system') %} +leapsecmode system +{% endif %} user {{ user }} diff --git a/data/templates/container/systemd-unit.j2 b/data/templates/container/systemd-unit.j2 index fa48384ab..d379f0a07 100644 --- a/data/templates/container/systemd-unit.j2 +++ b/data/templates/container/systemd-unit.j2 @@ -13,5 +13,5 @@ ExecStop=/usr/bin/podman stop --ignore --cidfile %t/%n.cid -t 5 ExecStopPost=/usr/bin/podman rm --ignore -f --cidfile %t/%n.cid ExecStopPost=/bin/rm -f %t/%n.cid PIDFile=%t/%n.pid -KillMode=none +KillMode=control-group Type=forking diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2 index 6ab13ab27..629fa952a 100644 --- a/data/templates/dhcp-server/kea-dhcp4.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -1,8 +1,16 @@ { "Dhcp4": { "interfaces-config": { +{% if listen_address is vyos_defined %} + "interfaces": {{ listen_address | kea_address_json }}, + "dhcp-socket-type": "udp", +{% elif listen_interface is vyos_defined %} + "interfaces": {{ listen_interface | tojson }}, + "dhcp-socket-type": "raw", +{% else %} "interfaces": [ "*" ], "dhcp-socket-type": "raw", +{% endif %} "service-sockets-max-retries": 5, "service-sockets-retry-wait-time": 5000 }, diff --git a/data/templates/dhcp-server/kea-dhcp6.conf.j2 b/data/templates/dhcp-server/kea-dhcp6.conf.j2 index 3ce4e6370..3ab21551b 100644 --- a/data/templates/dhcp-server/kea-dhcp6.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp6.conf.j2 @@ -1,7 +1,11 @@ { "Dhcp6": { "interfaces-config": { +{% if listen_interface is vyos_defined %} + "interfaces": {{ listen_interface | tojson }}, +{% else %} "interfaces": [ "*" ], +{% endif %} "service-sockets-max-retries": 5, "service-sockets-retry-wait-time": 5000 }, diff --git a/data/templates/dns-dynamic/ddclient.conf.j2 b/data/templates/dns-dynamic/ddclient.conf.j2 index 6c0653a55..5538ea56c 100644 --- a/data/templates/dns-dynamic/ddclient.conf.j2 +++ b/data/templates/dns-dynamic/ddclient.conf.j2 @@ -7,7 +7,7 @@ use{{ ipv }}={{ address if address == 'web' else 'if' }}{{ ipv }}, \ web{{ ipv }}={{ web_options.url }}, \ {% endif %} {% if web_options.skip is vyos_defined %} -web-skip{{ ipv }}='{{ web_options.skip }}', \ +web{{ ipv }}-skip='{{ web_options.skip }}', \ {% endif %} {% else %} if{{ ipv }}={{ address }}, \ @@ -45,9 +45,12 @@ use=no else ['']) %} {% set password = config.key if config.protocol == 'nsupdate' else config.password %} +{% set address = 'web' if config.address.web is vyos_defined + else config.address.interface %} +{% set web_options = config.address.web | default({}) %} # Web service dynamic DNS configuration for {{ service }}: [{{ config.protocol }}, {{ host }}] -{{ render_config(host, config.address, config.web_options, ip_suffixes, +{{ render_config(host, address, web_options, ip_suffixes, protocol=config.protocol, server=config.server, zone=config.zone, login=config.username, password=password, ttl=config.ttl, min_interval=config.wait_time, max_interval=config.expiry_time) }} diff --git a/data/templates/dns-forwarding/override.conf.j2 b/data/templates/dns-forwarding/override.conf.j2 new file mode 100644 index 000000000..9d81a2977 --- /dev/null +++ b/data/templates/dns-forwarding/override.conf.j2 @@ -0,0 +1,8 @@ +[Unit] +ConditionPathExists={{ config_file }} +After=vyos-router.service + +[Service] +RuntimeDirectoryPreserve=yes +ExecStart= +ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir={{ config_dir }} diff --git a/data/templates/dns-forwarding/recursor.conf.j2 b/data/templates/dns-forwarding/recursor.conf.j2 index ea700406c..5ac872f19 100644 --- a/data/templates/dns-forwarding/recursor.conf.j2 +++ b/data/templates/dns-forwarding/recursor.conf.j2 @@ -12,7 +12,7 @@ allow-from={{ allow_from | join(',') }} log-common-errors=yes non-local-bind=yes query-local-address={{ source_address | join(',') }} -lua-config-file=recursor.conf.lua +lua-config-file={{ config_dir }}/recursor.conf.lua # cache-size max-cache-entries={{ cache_size }} @@ -40,10 +40,34 @@ dnssec={{ dnssec }} dns64-prefix={{ dns64_prefix }} {% endif %} +{% if exclude_throttle_address is vyos_defined %} +# dont-throttle-netmasks +dont-throttle-netmasks={{ exclude_throttle_address | join(',') }} +{% endif %} + +{% if serve_stale_extension is vyos_defined %} +# serve-stale-extensions +serve-stale-extensions={{ serve_stale_extension }} +{% endif %} + # serve rfc1918 records serve-rfc1918={{ 'no' if no_serve_rfc1918 is vyos_defined else 'yes' }} # zones auth-zones={% for z in authoritative_zones %}{{ z.name }}={{ z.file }}{{- "," if not loop.last -}}{% endfor %} -forward-zones-file=recursor.forward-zones.conf +forward-zones-file={{ config_dir }}/recursor.forward-zones.conf + +#ecs +{% if options.ecs_add_for is vyos_defined %} +ecs-add-for={{ options.ecs_add_for | join(',') }} +{% endif %} + +{% if options.ecs_ipv4_bits is vyos_defined %} +ecs-ipv4-bits={{ options.ecs_ipv4_bits }} +{% endif %} + +{% if options.edns_subnet_allow_list is vyos_defined %} +edns-subnet-allow-list={{ options.edns_subnet_allow_list | join(',') }} +{% endif %} + diff --git a/data/templates/dns-forwarding/recursor.conf.lua.j2 b/data/templates/dns-forwarding/recursor.conf.lua.j2 index 816f69160..8026442c7 100644 --- a/data/templates/dns-forwarding/recursor.conf.lua.j2 +++ b/data/templates/dns-forwarding/recursor.conf.lua.j2 @@ -5,4 +5,4 @@ dofile("/usr/share/pdns-recursor/lua-config/rootkeys.lua") -- Load lua from vyos-hostsd -- -dofile("recursor.vyos-hostsd.conf.lua") +dofile("{{ config_dir }}/recursor.vyos-hostsd.conf.lua") diff --git a/data/templates/firewall/nftables-vrf-zones.j2 b/data/templates/firewall/nftables-vrf-zones.j2 deleted file mode 100644 index 3bce7312d..000000000 --- a/data/templates/firewall/nftables-vrf-zones.j2 +++ /dev/null @@ -1,17 +0,0 @@ -table inet vrf_zones { - # Map of interfaces and connections tracking zones - map ct_iface_map { - typeof iifname : ct zone - } - # Assign unique zones for each VRF - # Chain for inbound traffic - chain vrf_zones_ct_in { - type filter hook prerouting priority raw; policy accept; - counter ct original zone set iifname map @ct_iface_map - } - # Chain for locally-generated traffic - chain vrf_zones_ct_out { - type filter hook output priority raw; policy accept; - counter ct original zone set oifname map @ct_iface_map - } -} diff --git a/data/templates/firewall/nftables-zone.j2 b/data/templates/firewall/nftables-zone.j2 index 5e55099ca..e78725079 100644 --- a/data/templates/firewall/nftables-zone.j2 +++ b/data/templates/firewall/nftables-zone.j2 @@ -1,13 +1,6 @@ - -{% macro zone_chains(zone, family, state_policy=False) %} -{% if family == 'ipv6' %} -{% set fw_name = 'ipv6_name' %} -{% set suffix = '6' %} -{% else %} -{% set fw_name = 'name' %} -{% set suffix = '' %} -{% endif %} - +{% macro zone_chains(zone, ipv6=False, state_policy=False) %} +{% set fw_name = 'ipv6_name' if ipv6 else 'name' %} +{% set suffix = '6' if ipv6 else '' %} chain VYOS_ZONE_FORWARD { type filter hook forward priority 1; policy accept; {% if state_policy %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index e0ad0e00a..833df3a67 100644 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -163,7 +163,7 @@ table ip vyos_filter { {{ group_tmpl.groups(group, False, True) }} {% if zone is vyos_defined %} -{{ zone_tmpl.zone_chains(zone, 'ipv4', global_options.state_policy is vyos_defined) }} +{{ zone_tmpl.zone_chains(zone, False, global_options.state_policy is vyos_defined) }} {% endif %} {% if global_options.state_policy is vyos_defined %} chain VYOS_STATE_POLICY { @@ -298,7 +298,7 @@ table ip6 vyos_filter { {% endif %} {{ group_tmpl.groups(group, True, True) }} {% if zone is vyos_defined %} -{{ zone_tmpl.zone_chains(zone, 'ipv6', global_options.state_policy is vyos_defined) }} +{{ zone_tmpl.zone_chains(zone, True, global_options.state_policy is vyos_defined) }} {% endif %} {% if global_options.state_policy is vyos_defined %} chain VYOS_STATE_POLICY6 { diff --git a/data/templates/frr/bfdd.frr.j2 b/data/templates/frr/bfdd.frr.j2 index c4adeb402..f3303e401 100644 --- a/data/templates/frr/bfdd.frr.j2 +++ b/data/templates/frr/bfdd.frr.j2 @@ -13,6 +13,9 @@ bfd {% if profile_config.echo_mode is vyos_defined %} echo-mode {% endif %} +{% if profile_config.minimum_ttl is vyos_defined %} + minimum-ttl {{ profile_config.minimum_ttl }} +{% endif %} {% if profile_config.passive is vyos_defined %} passive-mode {% endif %} @@ -38,6 +41,9 @@ bfd {% if peer_config.echo_mode is vyos_defined %} echo-mode {% endif %} +{% if peer_config.minimum_ttl is vyos_defined %} + minimum-ttl {{ peer_config.minimum_ttl }} +{% endif %} {% if peer_config.passive is vyos_defined %} passive-mode {% endif %} diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 679ba8b04..e02fdd1bb 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -402,6 +402,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if afi_config.flooding.head_end_replication is vyos_defined %} flooding head-end-replication {% endif %} +{% if afi_config.nexthop.vpn.export is vyos_defined %} + nexthop vpn export {{ afi_config.nexthop.vpn.export }} +{% endif %} {% if afi_config.rd.vpn.export is vyos_defined %} rd vpn export {{ afi_config.rd.vpn.export }} {% endif %} @@ -436,6 +439,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if afi_config.route_map.vpn.import is vyos_defined %} route-map vpn import {{ afi_config.route_map.vpn.import }} {% endif %} +{% if afi_config.sid.vpn.export is vyos_defined %} + sid vpn export {{ afi_config.sid.vpn.export }} +{% endif %} {% if afi_config.vni is vyos_defined %} {% for vni, vni_config in afi_config.vni.items() %} vni {{ vni }} diff --git a/data/templates/grub/grub_common.j2 b/data/templates/grub/grub_common.j2 index 278ffbf2c..5e9b95cc0 100644 --- a/data/templates/grub/grub_common.j2 +++ b/data/templates/grub/grub_common.j2 @@ -8,9 +8,13 @@ fi function setup_serial { # initialize the first serial port by default if [ "${console_type}" == "ttyS" ]; then - serial --unit=${console_num} + if [ "${console_num}" == "0" ]; then + serial --unit=0 --speed=${console_speed} + else + serial --unit=${console_num} --speed=115200 + fi else - serial --unit=0 + serial --unit=0 --speed=${console_speed} fi terminal_output --append serial console terminal_input --append serial console diff --git a/data/templates/grub/grub_compat.j2 b/data/templates/grub/grub_compat.j2 index 887d5d0bd..d1085eec8 100644 --- a/data/templates/grub/grub_compat.j2 +++ b/data/templates/grub/grub_compat.j2 @@ -22,13 +22,13 @@ {%- endmacro %} {% macro console_opts(type) -%} {% if type == 'tty' -%} - console=ttyS0,115200 console=tty0 + console=ttyS0,{{ console_speed }} console=tty0 {%- elif type == 'ttyS' -%} - console=tty0 console=ttyS0,115200 + console=tty0 console=ttyS0,{{ console_speed }} {%- elif type == 'ttyUSB' -%} console=tty0 console=ttyUSB0,115200 {%- else -%} - console=tty0 console=ttyS0,115200 + console=tty0 console=ttyS0,{{ console_speed }} {%- endif %} {%- endmacro %} {% macro passwd_opts(mode) -%} @@ -39,9 +39,13 @@ set default={{ default }} set timeout={{ timeout }} {% if console_type == 'ttyS' %} +{% if console_num == '0' %} +serial --unit=0 --speed={{ console_speed }} +{% else %} serial --unit={{ console_num }} --speed=115200 +{% endif %} {% else %} -serial --unit=0 --speed=115200 +serial --unit=0 --speed={{ console_speed }} {% endif %} terminal_output --append serial terminal_input serial console diff --git a/data/templates/grub/grub_vyos_version.j2 b/data/templates/grub/grub_vyos_version.j2 index 97fbe8473..de85f1419 100644 --- a/data/templates/grub/grub_vyos_version.j2 +++ b/data/templates/grub/grub_vyos_version.j2 @@ -1,21 +1,31 @@ -{% set boot_opts_default = "boot=live rootdelay=5 noautologin net.ifnames=0 biosdevname=0 vyos-union=/boot/" + version_name %} -{% if boot_opts != '' %} +{% if boot_opts_config is vyos_defined %} +{% if boot_opts_config %} +{% set boot_opts_rendered = boot_opts_default + " " + boot_opts_config %} +{% else %} +{% set boot_opts_rendered = boot_opts_default %} +{% endif %} +{% elif boot_opts != '' %} {% set boot_opts_rendered = boot_opts %} {% else %} {% set boot_opts_rendered = boot_opts_default %} {% endif %} menuentry "{{ version_name }}" --id {{ version_uuid }} { set boot_opts="{{ boot_opts_rendered }}" + if [ "${console_type}" == "ttyS" ]; then + set console_opts="console=${console_type}${console_num},${console_speed}" + else + set console_opts="console=${console_type}${console_num}" + fi # load rootfs to RAM if [ "${boot_toram}" == "yes" ]; then set boot_opts="${boot_opts} toram" fi if [ "${bootmode}" == "pw_reset" ]; then - set boot_opts="${boot_opts} console=${console_type}${console_num} init=/usr/libexec/vyos/system/standalone_root_pw_reset" + set boot_opts="${boot_opts} ${console_opts} init=/usr/libexec/vyos/system/standalone_root_pw_reset" elif [ "${bootmode}" == "recovery" ]; then - set boot_opts="${boot_opts} console=${console_type}${console_num} init=/usr/bin/busybox init" + set boot_opts="${boot_opts} ${console_opts} init=/usr/bin/busybox init" else - set boot_opts="${boot_opts} console=${console_type}${console_num}" + set boot_opts="${boot_opts} ${console_opts}" fi linux "/boot/{{ version_name }}/vmlinuz" ${boot_opts} initrd "/boot/{{ version_name }}/initrd.img" diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2 index 80239ea56..5d17df001 100644 --- a/data/templates/https/nginx.default.j2 +++ b/data/templates/https/nginx.default.j2 @@ -1,60 +1,65 @@ ### Autogenerated by service_https.py ### -# Default server configuration -{% for server in server_block_list %} +{% if enable_http_redirect is vyos_defined %} server { - # SSL configuration - # -{% if server.address == '*' %} - listen {{ server.port }} ssl; - listen [::]:{{ server.port }} ssl; -{% else %} - listen {{ server.address | bracketize_ipv6 }}:{{ server.port }} ssl; -{% endif %} + listen 80 default_server; + server_name {{ hostname }}; + return 301 https://$host$request_uri; +} +{% endif %} -{% for name in server.name %} - server_name {{ name }}; +server { +{% if listen_address is vyos_defined %} +{% for address in listen_address %} + listen {{ address | bracketize_ipv6 }}:{{ port }} ssl; {% endfor %} +{% else %} + listen {{ port }} ssl; + listen [::]:{{ port }} ssl; +{% endif %} - root /srv/localui; + server_name {{ hostname }}; + root /srv/localui; -{% if server.certbot %} - ssl_certificate {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/fullchain.pem; - ssl_certificate_key {{ server.certbot_dir }}/live/{{ server.certbot_domain_dir }}/privkey.pem; - include {{ server.certbot_dir }}/options-ssl-nginx.conf; - ssl_dhparam {{ server.certbot_dir }}/ssl-dhparams.pem; -{% elif server.vyos_cert %} - ssl_certificate {{ server.vyos_cert.crt }}; - ssl_certificate_key {{ server.vyos_cert.key }}; -{% else %} - # - # Self signed certs generated by the ssl-cert package - # Don't use them in a production server! - # - include snippets/snakeoil.conf; + # SSL configuration +{% if certificates.cert_path is vyos_defined and certificates.key_path is vyos_defined %} + ssl_certificate {{ certificates.cert_path }}; + ssl_certificate_key {{ certificates.key_path }}; +{% if certificates.dh_file is vyos_defined %} + ssl_dhparam {{ certificates.dh_file }}; {% endif %} - ssl_protocols TLSv1.2 TLSv1.3; +{% else %} + # Self signed certs generated by the ssl-cert package + # Don't use them in a production server! + include snippets/snakeoil.conf; +{% endif %} - # proxy settings for HTTP API, if enabled; 503, if not - location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { -{% if server.api %} - proxy_pass http://unix:/run/api.sock; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - proxy_read_timeout 600; - proxy_buffering off; -{% else %} - return 503; -{% endif %} -{% if server.allow_client %} -{% for client in server.allow_client %} - allow {{ client }}; -{% endfor %} - deny all; -{% endif %} - } + # Improve HTTPS performance with session resumption + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 10m; + ssl_protocols {{ 'TLSv' ~ ' TLSv'.join(tls_version) }}; - error_page 497 =301 https://$host:{{ server.port }}$request_uri; -} + # From LetsEncrypt + ssl_prefer_server_ciphers on; + ssl_ciphers 'ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!3DES:!MD5:!PSK'; -{% endfor %} + # proxy settings for HTTP API, if enabled; 503, if not + location ~ ^/(retrieve|configure|config-file|image|container-image|generate|show|reboot|reset|poweroff|docs|openapi.json|redoc|graphql) { +{% if api is vyos_defined %} + proxy_pass http://unix:/run/api.sock; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_read_timeout 600; + proxy_buffering off; +{% else %} + return 503; +{% endif %} +{% if allow_client.address is vyos_defined %} +{% for address in allow_client.address %} + allow {{ address }}; +{% endfor %} + deny all; +{% endif %} + } + error_page 497 =301 https://$host:{{ port }}$request_uri; +} diff --git a/data/templates/https/vyos-http-api.service.j2 b/data/templates/https/vyos-http-api.service.j2 index f620b3248..aa4da7666 100644 --- a/data/templates/https/vyos-http-api.service.j2 +++ b/data/templates/https/vyos-http-api.service.j2 @@ -3,6 +3,7 @@ Description=VyOS HTTP API service After=vyos-router.service Requires=vyos-router.service +ConditionPathExists={{ api_config_state }} [Service] ExecStart={{ vrf_command }}/usr/libexec/vyos/services/vyos-http-api-server @@ -20,4 +21,3 @@ Group=vyattacfg [Install] WantedBy=vyos.target - diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2 index 9d95271fe..c5841fb91 100644 --- a/data/templates/ipsec/swanctl/peer.j2 +++ b/data/templates/ipsec/swanctl/peer.j2 @@ -83,10 +83,9 @@ start_action = none {% endif %} {% if ike.dead_peer_detection is vyos_defined %} -{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'restart'} %} - dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }} + dpd_action = {{ ike.dead_peer_detection.action }} {% endif %} - close_action = {{ {'none': 'none', 'hold': 'trap', 'restart': 'start'}[ike.close_action] }} + close_action = {{ ike.close_action }} } {% elif peer_conf.tunnel is vyos_defined %} {% for tunnel_id, tunnel_conf in peer_conf.tunnel.items() if tunnel_conf.disable is not defined %} @@ -134,10 +133,9 @@ start_action = none {% endif %} {% if ike.dead_peer_detection is vyos_defined %} -{% set dpd_translate = {'clear': 'clear', 'hold': 'trap', 'restart': 'restart'} %} - dpd_action = {{ dpd_translate[ike.dead_peer_detection.action] }} + dpd_action = {{ ike.dead_peer_detection.action }} {% endif %} - close_action = {{ {'none': 'none', 'hold': 'trap', 'restart': 'start'}[ike.close_action] }} + close_action = {{ ike.close_action }} {% if peer_conf.vti.bind is vyos_defined %} {# The key defaults to 0 and will match any policies which similarly do not have a lookup key configuration. #} {# Thus we simply shift the key by one to also support a vti0 interface #} diff --git a/data/templates/sflow/override.conf.j2 b/data/templates/sflow/override.conf.j2 index f2a982528..73588fdb2 100644 --- a/data/templates/sflow/override.conf.j2 +++ b/data/templates/sflow/override.conf.j2 @@ -1,3 +1,4 @@ +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} [Unit] After= After=vyos-router.service @@ -7,7 +8,7 @@ ConditionPathExists=/run/sflow/hsflowd.conf [Service] EnvironmentFile= ExecStart= -ExecStart=/usr/sbin/hsflowd -m %m -d -f /run/sflow/hsflowd.conf +ExecStart={{ vrf_command }}/usr/sbin/hsflowd -m %m -d -f /run/sflow/hsflowd.conf WorkingDirectory= WorkingDirectory=/run/sflow PIDFile= diff --git a/data/vyos-firewall-init.conf b/data/vyos-firewall-init.conf index cd7d5011f..5a4e03015 100644 --- a/data/vyos-firewall-init.conf +++ b/data/vyos-firewall-init.conf @@ -54,3 +54,22 @@ table ip6 raw { type filter hook prerouting priority -300; policy accept; } } + +# Required by VRF +table inet vrf_zones { + # Map of interfaces and connections tracking zones + map ct_iface_map { + typeof iifname : ct zone + } + # Assign unique zones for each VRF + # Chain for inbound traffic + chain vrf_zones_ct_in { + type filter hook prerouting priority raw; policy accept; + counter ct original zone set iifname map @ct_iface_map + } + # Chain for locally-generated traffic + chain vrf_zones_ct_out { + type filter hook output priority raw; policy accept; + counter ct original zone set oifname map @ct_iface_map + } +} diff --git a/debian/control b/debian/control index af42202fc..726a083f2 100644 --- a/debian/control +++ b/debian/control @@ -43,7 +43,6 @@ Depends: ## End of Fundamentals ## Python libraries used in multiple modules and scripts python3, - python3-certbot-nginx, python3-cryptography, python3-hurry.filesize, python3-inotify, @@ -146,6 +145,9 @@ Depends: # For "protocols igmp-proxy" igmpproxy, # End "protocols igmp-proxy" +# For "pki" + certbot, +# End "pki" # For "service console-server" conserver-client, conserver-server, diff --git a/debian/rules b/debian/rules index 9a6ab2996..d007089a4 100755 --- a/debian/rules +++ b/debian/rules @@ -24,7 +24,7 @@ DEB_TARGET_ARCH := $(shell dpkg-architecture -qDEB_TARGET_ARCH) override_dh_strip_nondeterminism: override_dh_gencontrol: - dh_gencontrol -- -v$(shell (git describe --tags --long --match 'vyos/*' --dirty 2>/dev/null || echo 0.0-no.git.tag) | sed -E 's%vyos/%%' | sed -E 's%-dirty%+dirty%') + dh_gencontrol -- -v$(shell (git describe --tags --long --match 'vyos/*' --match '1.4.*' --dirty 2>/dev/null || echo 0.0-no.git.tag) | sed -E 's%vyos/%%' | sed -E 's%-dirty%+dirty%') override_dh_auto_build: make all diff --git a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i index 71fe69f8d..b30a5ee01 100644 --- a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i +++ b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i @@ -27,11 +27,15 @@ <validator name="ipv4-host"/> <validator name="ipv4-range-mask" argument="-m 24 -r"/> </constraint> + <multi/> </properties> </leafNode> <leafNode name="next-pool"> <properties> <help>Next pool name</help> + <completionHelp> + <path>${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-4}</path> + </completionHelp> <valueHelp> <format>txt</format> <description>Name of IP pool</description> diff --git a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i index 774741a5e..0c8c2e34c 100644 --- a/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i +++ b/interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i @@ -1,7 +1,14 @@ <!-- include start from accel-ppp/client-ipv6-pool.xml.i --> -<node name="client-ipv6-pool"> +<tagNode name="client-ipv6-pool"> <properties> <help>Pool of client IPv6 addresses</help> + <valueHelp> + <format>txt</format> + <description>Name of IPv6 pool</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> </properties> <children> <tagNode name="prefix"> @@ -58,5 +65,5 @@ </children> </tagNode> </children> -</node> +</tagNode> <!-- include end --> diff --git a/interface-definitions/include/accel-ppp/default-ipv6-pool.xml.i b/interface-definitions/include/accel-ppp/default-ipv6-pool.xml.i new file mode 100644 index 000000000..1093f6713 --- /dev/null +++ b/interface-definitions/include/accel-ppp/default-ipv6-pool.xml.i @@ -0,0 +1,17 @@ +<!-- include start from accel-ppp/default-pool.xml.i --> +<leafNode name="default-ipv6-pool"> + <properties> + <help>Default client IPv6 pool name</help> + <completionHelp> + <path>${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-3} client-ipv6-pool</path> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Default IPv6 pool</description> + </valueHelp> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/accel-ppp/default-pool.xml.i b/interface-definitions/include/accel-ppp/default-pool.xml.i index a08b066b1..e06642c37 100644 --- a/interface-definitions/include/accel-ppp/default-pool.xml.i +++ b/interface-definitions/include/accel-ppp/default-pool.xml.i @@ -2,6 +2,9 @@ <leafNode name="default-pool"> <properties> <help>Default client IP pool name</help> + <completionHelp> + <path>${COMP_WORDS[@]:1:${#COMP_WORDS[@]}-3} client-ip-pool</path> + </completionHelp> <valueHelp> <format>txt</format> <description>Default IP pool</description> diff --git a/interface-definitions/include/bfd/common.xml.i b/interface-definitions/include/bfd/common.xml.i index 126ab9b9a..8e6999d28 100644 --- a/interface-definitions/include/bfd/common.xml.i +++ b/interface-definitions/include/bfd/common.xml.i @@ -63,6 +63,18 @@ </leafNode> </children> </node> +<leafNode name="minimum-ttl"> + <properties> + <help>Expect packets with at least this TTL</help> + <valueHelp> + <format>u32:1-254</format> + <description>Minimum TTL expected</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-254"/> + </constraint> + </properties> +</leafNode> <leafNode name="passive"> <properties> <help>Do not attempt to start sessions</help> diff --git a/interface-definitions/include/bgp/afi-nexthop-vpn-export.xml.i b/interface-definitions/include/bgp/afi-nexthop-vpn-export.xml.i new file mode 100644 index 000000000..d90597f37 --- /dev/null +++ b/interface-definitions/include/bgp/afi-nexthop-vpn-export.xml.i @@ -0,0 +1,32 @@ +<!-- include start from bgp/afi-nexthop-vpn-export.xml.i --> +<node name="nexthop"> + <properties> + <help>Specify next hop to use for VRF advertised prefixes</help> + </properties> + <children> + <node name="vpn"> + <properties> + <help>Between current address-family and vpn</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>For routes leaked from current address-family to vpn</help> + <valueHelp> + <format>ipv4</format> + <description>BGP neighbor IP address</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>BGP neighbor IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> +</node> + <!-- include end --> diff --git a/interface-definitions/include/bgp/afi-sid.xml.i b/interface-definitions/include/bgp/afi-sid.xml.i new file mode 100644 index 000000000..38a3dcf9b --- /dev/null +++ b/interface-definitions/include/bgp/afi-sid.xml.i @@ -0,0 +1,36 @@ +<!-- include start from bgp/sid.xml.i --> +<node name="sid"> + <properties> + <help>SID value for VRF</help> + </properties> + <children> + <node name="vpn"> + <properties> + <help>Between current VRF and VPN</help> + </properties> + <children> + <leafNode name="export"> + <properties> + <help>For routes leaked from current VRF to VPN</help> + <completionHelp> + <list>auto</list> + </completionHelp> + <valueHelp> + <format>u32:1-1048575</format> + <description>SID allocation index</description> + </valueHelp> + <valueHelp> + <format>auto</format> + <description>Automatically assign a label</description> + </valueHelp> + <constraint> + <regex>auto</regex> + <validator name="numeric" argument="--range 1-1048575"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <!-- include end --> diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i index dce61ee77..9895b025c 100644 --- a/interface-definitions/include/bgp/protocol-common-config.xml.i +++ b/interface-definitions/include/bgp/protocol-common-config.xml.i @@ -120,6 +120,7 @@ #include <include/bgp/afi-rd.xml.i> #include <include/bgp/afi-route-map-vpn.xml.i> #include <include/bgp/afi-route-target-vpn.xml.i> + #include <include/bgp/afi-nexthop-vpn-export.xml.i> <node name="redistribute"> <properties> <help>Redistribute routes from other protocols into BGP</help> @@ -188,6 +189,7 @@ </leafNode> </children> </node> + #include <include/bgp/afi-sid.xml.i> </children> </node> <node name="ipv4-multicast"> @@ -495,6 +497,7 @@ #include <include/bgp/afi-rd.xml.i> #include <include/bgp/afi-route-map-vpn.xml.i> #include <include/bgp/afi-route-target-vpn.xml.i> + #include <include/bgp/afi-nexthop-vpn-export.xml.i> <node name="redistribute"> <properties> <help>Redistribute routes from other protocols into BGP</help> @@ -555,6 +558,7 @@ </leafNode> </children> </node> + #include <include/bgp/afi-sid.xml.i> </children> </node> <node name="ipv6-multicast"> @@ -1698,8 +1702,10 @@ </properties> <children> #include <include/bgp/neighbor-afi-ipv4-unicast.xml.i> + #include <include/bgp/neighbor-afi-ipv4-labeled-unicast.xml.i> #include <include/bgp/neighbor-afi-ipv4-vpn.xml.i> #include <include/bgp/neighbor-afi-ipv6-unicast.xml.i> + #include <include/bgp/neighbor-afi-ipv6-labeled-unicast.xml.i> #include <include/bgp/neighbor-afi-ipv6-vpn.xml.i> #include <include/bgp/neighbor-afi-l2vpn-evpn.xml.i> </children> diff --git a/interface-definitions/include/constraint/email.xml.i b/interface-definitions/include/constraint/email.xml.i new file mode 100644 index 000000000..b19a88d64 --- /dev/null +++ b/interface-definitions/include/constraint/email.xml.i @@ -0,0 +1,3 @@ +<!-- include start from constraint/email.xml.i --> +<regex>[^\s@]+@([^\s@.,]+\.)+[^\s@.,]{2,}</regex> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i new file mode 100644 index 000000000..bd6fc6043 --- /dev/null +++ b/interface-definitions/include/dhcp/option-v4.xml.i @@ -0,0 +1,257 @@ +<!-- include start from dhcp/option-v4.xml.i --> +<node name="option"> + <properties> + <help>DHCP option</help> + </properties> + <children> + #include <include/dhcp/captive-portal.xml.i> + #include <include/dhcp/domain-name.xml.i> + #include <include/dhcp/domain-search.xml.i> + #include <include/dhcp/ntp-server.xml.i> + #include <include/name-server-ipv4.xml.i> + <leafNode name="bootfile-name"> + <properties> + <help>Bootstrap file name</help> + <constraint> + <regex>[[:ascii:]]{1,253}</regex> + </constraint> + </properties> + </leafNode> + <leafNode name="bootfile-server"> + <properties> + <help>Server from which the initial boot file is to be loaded</help> + <valueHelp> + <format>ipv4</format> + <description>Bootfile server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>Bootfile server FQDN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + <leafNode name="bootfile-size"> + <properties> + <help>Bootstrap file size</help> + <valueHelp> + <format>u32:1-16</format> + <description>Bootstrap file size in 512 byte blocks</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-16"/> + </constraint> + </properties> + </leafNode> + <leafNode name="client-prefix-length"> + <properties> + <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> + <valueHelp> + <format>u32:0-32</format> + <description>DHCP client prefix length must be 0 to 32</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="default-router"> + <properties> + <help>IP address of default router</help> + <valueHelp> + <format>ipv4</format> + <description>Default router IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="ip-forwarding"> + <properties> + <help>Enable IP forwarding on client</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ipv6-only-preferred"> + <properties> + <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help> + <valueHelp> + <format>u32</format> + <description>Seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-4294967295"/> + </constraint> + <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="pop-server"> + <properties> + <help>IP address of POP3 server</help> + <valueHelp> + <format>ipv4</format> + <description>POP3 server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="server-identifier"> + <properties> + <help>Address for DHCP server identifier</help> + <valueHelp> + <format>ipv4</format> + <description>DHCP server identifier IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + <leafNode name="smtp-server"> + <properties> + <help>IP address of SMTP server</help> + <valueHelp> + <format>ipv4</format> + <description>SMTP server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <tagNode name="static-route"> + <properties> + <help>Classless static route destination subnet</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 address and prefix length</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="next-hop"> + <properties> + <help>IP address of router to be used to reach the destination subnet</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address of router</description> + </valueHelp> + <constraint> + <validator name="ip-address"/> + </constraint> + </properties> + </leafNode> + </children> + </tagNode > + <leafNode name="tftp-server-name"> + <properties> + <help>TFTP server name</help> + <valueHelp> + <format>ipv4</format> + <description>TFTP server IPv4 address</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>TFTP server FQDN</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + <validator name="fqdn"/> + </constraint> + </properties> + </leafNode> + <leafNode name="time-offset"> + <properties> + <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help> + <valueHelp> + <format>[-]N</format> + <description>Time offset (number, may be negative)</description> + </valueHelp> + <constraint> + <regex>-?[0-9]+</regex> + </constraint> + <constraintErrorMessage>Invalid time offset value</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="time-server"> + <properties> + <help>IP address of time server</help> + <valueHelp> + <format>ipv4</format> + <description>Time server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="time-zone"> + <properties> + <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help> + <completionHelp> + <script>timedatectl list-timezones</script> + </completionHelp> + <constraint> + <validator name="timezone" argument="--validate"/> + </constraint> + </properties> + </leafNode> + <node name="vendor-option"> + <properties> + <help>Vendor Specific Options</help> + </properties> + <children> + <node name="ubiquiti"> + <properties> + <help>Ubiquiti specific parameters</help> + </properties> + <children> + <leafNode name="unifi-controller"> + <properties> + <help>Address of UniFi controller</help> + <valueHelp> + <format>ipv4</format> + <description>IP address of UniFi controller</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + <leafNode name="wins-server"> + <properties> + <help>IP address for Windows Internet Name Service (WINS) server</help> + <valueHelp> + <format>ipv4</format> + <description>WINS server IPv4 address</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="wpad-url"> + <properties> + <help>Web Proxy Autodiscovery (WPAD) URL</help> + </properties> + </leafNode> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/dhcp/option-v6.xml.i b/interface-definitions/include/dhcp/option-v6.xml.i new file mode 100644 index 000000000..1df0c3934 --- /dev/null +++ b/interface-definitions/include/dhcp/option-v6.xml.i @@ -0,0 +1,110 @@ +<!-- include start from dhcp/option-v6.xml.i --> +<node name="option"> + <properties> + <help>DHCPv6 option</help> + </properties> + <children> + #include <include/dhcp/captive-portal.xml.i> + #include <include/dhcp/domain-search.xml.i> + #include <include/name-server-ipv6.xml.i> + <leafNode name="nis-domain"> + <properties> + <help>NIS domain name for client to use</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nis-server"> + <properties> + <help>IPv6 address of a NIS Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="nisplus-domain"> + <properties> + <help>NIS+ domain name for client to use</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage> + </properties> + </leafNode> + <leafNode name="nisplus-server"> + <properties> + <help>IPv6 address of a NIS+ Server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of NIS+ server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="sip-server"> + <properties> + <help>IPv6 address of SIP server</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address of SIP server</description> + </valueHelp> + <valueHelp> + <format>hostname</format> + <description>FQDN of SIP server</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + <validator name="fqdn"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="sntp-server"> + <properties> + <help>IPv6 address of an SNTP server for client to use</help> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + <node name="vendor-option"> + <properties> + <help>Vendor Specific Options</help> + </properties> + <children> + <node name="cisco"> + <properties> + <help>Cisco specific parameters</help> + </properties> + <children> + <leafNode name="tftp-server"> + <properties> + <help>TFTP server name</help> + <valueHelp> + <format>ipv6</format> + <description>TFTP server IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + <multi/> + </properties> + </leafNode> + </children> + </node> + </children> + </node> + </children> +</node> +<!-- include end --> diff --git a/interface-definitions/include/firewall/common-rule-inet.xml.i b/interface-definitions/include/firewall/common-rule-inet.xml.i index 6f56ecc85..85189d975 100644 --- a/interface-definitions/include/firewall/common-rule-inet.xml.i +++ b/interface-definitions/include/firewall/common-rule-inet.xml.i @@ -32,25 +32,6 @@ </leafNode> </children> </node> -<node name="ipsec"> - <properties> - <help>Inbound IPsec packets</help> - </properties> - <children> - <leafNode name="match-ipsec"> - <properties> - <help>Inbound IPsec packets</help> - <valueless/> - </properties> - </leafNode> - <leafNode name="match-none"> - <properties> - <help>Inbound non-IPsec packets</help> - <valueless/> - </properties> - </leafNode> - </children> -</node> <node name="limit"> <properties> <help>Rate limit using a token bucket filter</help> diff --git a/interface-definitions/include/firewall/ipv4-custom-name.xml.i b/interface-definitions/include/firewall/ipv4-custom-name.xml.i index 8199d15fe..8046b2d6c 100644 --- a/interface-definitions/include/firewall/ipv4-custom-name.xml.i +++ b/interface-definitions/include/firewall/ipv4-custom-name.xml.i @@ -33,6 +33,7 @@ <children> #include <include/firewall/common-rule-ipv4.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> #include <include/firewall/offload-target.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> diff --git a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i index de2c70482..b0e240a03 100644 --- a/interface-definitions/include/firewall/ipv4-hook-forward.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-forward.xml.i @@ -28,6 +28,7 @@ #include <include/firewall/action-forward.xml.i> #include <include/firewall/common-rule-ipv4.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> #include <include/firewall/offload-target.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> diff --git a/interface-definitions/include/firewall/ipv4-hook-input.xml.i b/interface-definitions/include/firewall/ipv4-hook-input.xml.i index 5d32657ea..cefb1ffa7 100644 --- a/interface-definitions/include/firewall/ipv4-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv4-hook-input.xml.i @@ -27,6 +27,7 @@ <children> #include <include/firewall/common-rule-ipv4.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/firewall/ipv6-custom-name.xml.i b/interface-definitions/include/firewall/ipv6-custom-name.xml.i index 5748b3927..fb8740c38 100644 --- a/interface-definitions/include/firewall/ipv6-custom-name.xml.i +++ b/interface-definitions/include/firewall/ipv6-custom-name.xml.i @@ -33,6 +33,7 @@ <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> #include <include/firewall/offload-target.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> diff --git a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i index b53f09f59..7efc2614e 100644 --- a/interface-definitions/include/firewall/ipv6-hook-forward.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-forward.xml.i @@ -28,6 +28,7 @@ #include <include/firewall/action-forward.xml.i> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> #include <include/firewall/offload-target.xml.i> #include <include/firewall/outbound-interface.xml.i> </children> diff --git a/interface-definitions/include/firewall/ipv6-hook-input.xml.i b/interface-definitions/include/firewall/ipv6-hook-input.xml.i index 493611fb1..e1f41e64c 100644 --- a/interface-definitions/include/firewall/ipv6-hook-input.xml.i +++ b/interface-definitions/include/firewall/ipv6-hook-input.xml.i @@ -27,6 +27,7 @@ <children> #include <include/firewall/common-rule-ipv6.xml.i> #include <include/firewall/inbound-interface.xml.i> + #include <include/firewall/match-ipsec.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/include/firewall/match-ipsec.xml.i b/interface-definitions/include/firewall/match-ipsec.xml.i new file mode 100644 index 000000000..82c2b324d --- /dev/null +++ b/interface-definitions/include/firewall/match-ipsec.xml.i @@ -0,0 +1,21 @@ +<!-- include start from firewall/match-ipsec.xml.i --> +<node name="ipsec"> + <properties> + <help>Inbound IPsec packets</help> + </properties> + <children> + <leafNode name="match-ipsec"> + <properties> + <help>Inbound IPsec packets</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="match-none"> + <properties> + <help>Inbound non-IPsec packets</help> + <valueless/> + </properties> + </leafNode> + </children> +</node> +<!-- include end -->
\ No newline at end of file diff --git a/interface-definitions/include/haproxy/rule-backend.xml.i b/interface-definitions/include/haproxy/rule-backend.xml.i index a6832d693..b2be4fde4 100644 --- a/interface-definitions/include/haproxy/rule-backend.xml.i +++ b/interface-definitions/include/haproxy/rule-backend.xml.i @@ -118,7 +118,7 @@ <description>Exactly URL</description> </valueHelp> <constraint> - <regex>^\/[\w\-.\/]+$</regex> + <regex>^\/[\w\-.\/]*$</regex> </constraint> <constraintErrorMessage>Incorrect URL format</constraintErrorMessage> <multi/> diff --git a/interface-definitions/include/listen-interface-multi-broadcast.xml.i b/interface-definitions/include/listen-interface-multi-broadcast.xml.i new file mode 100644 index 000000000..b3d5a3ecc --- /dev/null +++ b/interface-definitions/include/listen-interface-multi-broadcast.xml.i @@ -0,0 +1,18 @@ +<!-- include start from listen-interface-multi-broadcast.xml.i --> +<leafNode name="listen-interface"> + <properties> + <help>Interface for DHCP Relay Agent to listen for requests</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces --broadcast</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Interface name</description> + </valueHelp> + <constraint> + #include <include/constraint/interface-name.xml.i> + </constraint> + <multi/> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/pki/dh-params.xml.i b/interface-definitions/include/pki/dh-params.xml.i new file mode 100644 index 000000000..a422df832 --- /dev/null +++ b/interface-definitions/include/pki/dh-params.xml.i @@ -0,0 +1,10 @@ +<!-- include start from pki/certificate-multi.xml.i --> +<leafNode name="dh-params"> + <properties> + <help>Diffie Hellman parameters (server only)</help> + <completionHelp> + <path>pki dh</path> + </completionHelp> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/hfsc-m1.xml.i b/interface-definitions/include/qos/hfsc-m1.xml.i index 677d817ba..21b9c4f32 100644 --- a/interface-definitions/include/qos/hfsc-m1.xml.i +++ b/interface-definitions/include/qos/hfsc-m1.xml.i @@ -27,6 +27,6 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>100%%</defaultValue> + <defaultValue>0bit</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/qos/hfsc-m2.xml.i b/interface-definitions/include/qos/hfsc-m2.xml.i index 7690df4b0..24e8f5d63 100644 --- a/interface-definitions/include/qos/hfsc-m2.xml.i +++ b/interface-definitions/include/qos/hfsc-m2.xml.i @@ -27,6 +27,6 @@ <description>bps(8),kbps(8*10^3),mbps(8*10^6), gbps, tbps - Byte/sec</description> </valueHelp> </properties> - <defaultValue>100%%</defaultValue> + <defaultValue>100%</defaultValue> </leafNode> <!-- include end --> diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i index 1386ea9bc..6bed7189f 100644 --- a/interface-definitions/include/version/bgp-version.xml.i +++ b/interface-definitions/include/version/bgp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/bgp-version.xml.i --> -<syntaxVersion component='bgp' version='4'></syntaxVersion> +<syntaxVersion component='bgp' version='5'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/conntrack-version.xml.i b/interface-definitions/include/version/conntrack-version.xml.i index c0f632c70..6995ce119 100644 --- a/interface-definitions/include/version/conntrack-version.xml.i +++ b/interface-definitions/include/version/conntrack-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/conntrack-version.xml.i --> -<syntaxVersion component='conntrack' version='4'></syntaxVersion> +<syntaxVersion component='conntrack' version='5'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index cc84ea8b9..d83172e72 100644 --- a/interface-definitions/include/version/dhcp-server-version.xml.i +++ b/interface-definitions/include/version/dhcp-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcp-server-version.xml.i --> -<syntaxVersion component='dhcp-server' version='8'></syntaxVersion> +<syntaxVersion component='dhcp-server' version='9'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/dhcpv6-server-version.xml.i b/interface-definitions/include/version/dhcpv6-server-version.xml.i index cb026a54a..bfef27b77 100644 --- a/interface-definitions/include/version/dhcpv6-server-version.xml.i +++ b/interface-definitions/include/version/dhcpv6-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcpv6-server-version.xml.i --> -<syntaxVersion component='dhcpv6-server' version='3'></syntaxVersion> +<syntaxVersion component='dhcpv6-server' version='4'></syntaxVersion> <!-- 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 773a6ab51..346385ccb 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='3'></syntaxVersion> +<syntaxVersion component='dns-dynamic' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/https-version.xml.i b/interface-definitions/include/version/https-version.xml.i index fa18278f3..525314dbd 100644 --- a/interface-definitions/include/version/https-version.xml.i +++ b/interface-definitions/include/version/https-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/https-version.xml.i --> -<syntaxVersion component='https' version='5'></syntaxVersion> +<syntaxVersion component='https' version='6'></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 e5983ab39..659433382 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='2'></syntaxVersion> +<syntaxVersion component='ipoe-server' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/ipsec-version.xml.i b/interface-definitions/include/version/ipsec-version.xml.i index de7a9c088..a4d556cfc 100644 --- a/interface-definitions/include/version/ipsec-version.xml.i +++ b/interface-definitions/include/version/ipsec-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/ipsec-version.xml.i --> -<syntaxVersion component='ipsec' version='12'></syntaxVersion> +<syntaxVersion component='ipsec' version='13'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/l2tp-version.xml.i b/interface-definitions/include/version/l2tp-version.xml.i index f4507d93b..793cd5d0c 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='6'></syntaxVersion> +<syntaxVersion component='l2tp' version='7'></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 deed702f0..02f98cc16 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='7'></syntaxVersion> +<syntaxVersion component='pppoe-server' version='8'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/sstp-version.xml.i b/interface-definitions/include/version/sstp-version.xml.i index 3ac54a3de..5e30950d8 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='5'></syntaxVersion> +<syntaxVersion component='sstp' version='6'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in index addf3c1ab..389b5b5c9 100644 --- a/interface-definitions/interfaces_openvpn.xml.in +++ b/interface-definitions/interfaces_openvpn.xml.in @@ -720,14 +720,7 @@ </leafNode> #include <include/pki/certificate.xml.i> #include <include/pki/ca-certificate-multi.xml.i> - <leafNode name="dh-params"> - <properties> - <help>Diffie Hellman parameters (server only)</help> - <completionHelp> - <path>pki dh</path> - </completionHelp> - </properties> - </leafNode> + #include <include/pki/dh-params.xml.i> <leafNode name="crypt-key"> <properties> <help>Static key to use to authenticate control channel</help> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 097c541ac..0ed199539 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -81,6 +81,60 @@ <constraintErrorMessage>Certificate is not base64-encoded</constraintErrorMessage> </properties> </leafNode> + <node name="acme"> + <properties> + <help>Automatic Certificate Management Environment (ACME) request</help> + </properties> + <children> + #include <include/url-http-https.xml.i> + <leafNode name="url"> + <defaultValue>https://acme-v02.api.letsencrypt.org/directory</defaultValue> + </leafNode> + <leafNode name="domain-name"> + <properties> + <help>Domain Name</help> + <constraint> + <validator name="fqdn"/> + </constraint> + <constraintErrorMessage>Invalid domain name (RFC 1123 section 2).\nMay only contain letters, numbers and .-_</constraintErrorMessage> + <multi/> + </properties> + </leafNode> + <leafNode name="email"> + <properties> + <help>Email address to associate with certificate</help> + <constraint> + #include <include/constraint/email.xml.i> + </constraint> + </properties> + </leafNode> + #include <include/listen-address-ipv4-single.xml.i> + <leafNode name="rsa-key-size"> + <properties> + <help>Size of the RSA key</help> + <completionHelp> + <list>2048 3072 4096</list> + </completionHelp> + <valueHelp> + <format>2048</format> + <description>RSA key length 2048 bit</description> + </valueHelp> + <valueHelp> + <format>3072</format> + <description>RSA key length 3072 bit</description> + </valueHelp> + <valueHelp> + <format>4096</format> + <description>RSA key length 4096 bit</description> + </valueHelp> + <constraint> + <regex>(2048|3072|4096)</regex> + </constraint> + </properties> + <defaultValue>2048</defaultValue> + </leafNode> + </children> + </node> #include <include/generic-description.xml.i> <node name="private"> <properties> diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index 8e13f9372..5c9d4a360 100644 --- a/interface-definitions/service_dhcp-server.xml.in +++ b/interface-definitions/service_dhcp-server.xml.in @@ -74,6 +74,7 @@ </properties> </leafNode> #include <include/listen-address-ipv4.xml.i> + #include <include/listen-interface-multi-broadcast.xml.i> <tagNode name="shared-network-name"> <properties> <help>Name of DHCP shared network</help> @@ -89,12 +90,9 @@ <valueless/> </properties> </leafNode> - #include <include/dhcp/domain-name.xml.i> - #include <include/dhcp/domain-search.xml.i> - #include <include/dhcp/ntp-server.xml.i> + #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> - #include <include/name-server-ipv4.xml.i> <tagNode name="subnet"> <properties> <help>DHCP subnet for shared network</help> @@ -108,73 +106,9 @@ <constraintErrorMessage>Invalid IPv4 subnet definition</constraintErrorMessage> </properties> <children> - <leafNode name="bootfile-name"> - <properties> - <help>Bootstrap file name</help> - <constraint> - <regex>[[:ascii:]]{1,253}</regex> - </constraint> - </properties> - </leafNode> - <leafNode name="bootfile-server"> - <properties> - <help>Server from which the initial boot file is to be loaded</help> - <valueHelp> - <format>ipv4</format> - <description>Bootfile server IPv4 address</description> - </valueHelp> - <valueHelp> - <format>hostname</format> - <description>Bootfile server FQDN</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - <validator name="fqdn"/> - </constraint> - </properties> - </leafNode> - <leafNode name="bootfile-size"> - <properties> - <help>Bootstrap file size</help> - <valueHelp> - <format>u32:1-16</format> - <description>Bootstrap file size in 512 byte blocks</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-16"/> - </constraint> - </properties> - </leafNode> - #include <include/dhcp/captive-portal.xml.i> - <leafNode name="client-prefix-length"> - <properties> - <help>Specifies the clients subnet mask as per RFC 950. If unset, subnet declaration is used.</help> - <valueHelp> - <format>u32:0-32</format> - <description>DHCP client prefix length must be 0 to 32</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-32"/> - </constraint> - <constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="default-router"> - <properties> - <help>IP address of default router</help> - <valueHelp> - <format>ipv4</format> - <description>Default router IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - #include <include/dhcp/domain-name.xml.i> - #include <include/dhcp/domain-search.xml.i> + #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> - #include <include/name-server-ipv4.xml.i> + #include <include/generic-disable-node.xml.i> <leafNode name="exclude"> <properties> <help>IP address to exclude from DHCP lease range</help> @@ -188,12 +122,6 @@ <multi/> </properties> </leafNode> - <leafNode name="ip-forwarding"> - <properties> - <help>Enable IP forwarding on client</help> - <valueless/> - </properties> - </leafNode> <leafNode name="lease"> <properties> <help>Lease timeout in seconds</help> @@ -208,45 +136,6 @@ </properties> <defaultValue>86400</defaultValue> </leafNode> - #include <include/dhcp/ntp-server.xml.i> - <leafNode name="pop-server"> - <properties> - <help>IP address of POP3 server</help> - <valueHelp> - <format>ipv4</format> - <description>POP3 server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="server-identifier"> - <properties> - <help>Address for DHCP server identifier</help> - <valueHelp> - <format>ipv4</format> - <description>DHCP server identifier IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - <leafNode name="smtp-server"> - <properties> - <help>IP address of SMTP server</help> - <valueHelp> - <format>ipv4</format> - <description>SMTP server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> <tagNode name="range"> <properties> <help>DHCP lease range</help> @@ -256,6 +145,7 @@ <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v4.xml.i> <leafNode name="start"> <properties> <help>First IP address for DHCP lease range</help> @@ -291,6 +181,8 @@ <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v4.xml.i> + #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> <leafNode name="ip-address"> <properties> @@ -308,143 +200,18 @@ #include <include/interface/duid.xml.i> </children> </tagNode> - <tagNode name="static-route"> - <properties> - <help>Classless static route destination subnet</help> - <valueHelp> - <format>ipv4net</format> - <description>IPv4 address and prefix length</description> - </valueHelp> - <constraint> - <validator name="ipv4-prefix"/> - </constraint> - </properties> - <children> - <leafNode name="next-hop"> - <properties> - <help>IP address of router to be used to reach the destination subnet</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address of router</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode > - <leafNode name="ipv6-only-preferred"> + <leafNode name="subnet-id"> <properties> - <help>Disable IPv4 on IPv6 only hosts (RFC 8925)</help> + <help>Unique ID mapped to leases in the lease file</help> <valueHelp> <format>u32</format> - <description>Seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 0-4294967295"/> - </constraint> - <constraintErrorMessage>Seconds must be between 0 and 4294967295 (49 days)</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="tftp-server-name"> - <properties> - <help>TFTP server name</help> - <valueHelp> - <format>ipv4</format> - <description>TFTP server IPv4 address</description> - </valueHelp> - <valueHelp> - <format>hostname</format> - <description>TFTP server FQDN</description> + <description>Unique subnet ID</description> </valueHelp> <constraint> - <validator name="ipv4-address"/> - <validator name="fqdn"/> + <validator name="numeric" argument="--range 1-4294967295"/> </constraint> </properties> </leafNode> - <leafNode name="time-offset"> - <properties> - <help>Client subnet offset in seconds from Coordinated Universal Time (UTC)</help> - <valueHelp> - <format>[-]N</format> - <description>Time offset (number, may be negative)</description> - </valueHelp> - <constraint> - <regex>-?[0-9]+</regex> - </constraint> - <constraintErrorMessage>Invalid time offset value</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="time-server"> - <properties> - <help>IP address of time server</help> - <valueHelp> - <format>ipv4</format> - <description>Time server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="time-zone"> - <properties> - <help>Time zone to send to clients. Uses RFC4833 options 100 and 101</help> - <completionHelp> - <script>timedatectl list-timezones</script> - </completionHelp> - <constraint> - <validator name="timezone" argument="--validate"/> - </constraint> - </properties> - </leafNode> - <node name="vendor-option"> - <properties> - <help>Vendor Specific Options</help> - </properties> - <children> - <node name="ubiquiti"> - <properties> - <help>Ubiquiti specific parameters</help> - </properties> - <children> - <leafNode name="unifi-controller"> - <properties> - <help>Address of UniFi controller</help> - <valueHelp> - <format>ipv4</format> - <description>IP address of UniFi controller</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> - </children> - </node> - </children> - </node> - <leafNode name="wins-server"> - <properties> - <help>IP address for Windows Internet Name Service (WINS) server</help> - <valueHelp> - <format>ipv4</format> - <description>WINS server IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="wpad-url"> - <properties> - <help>Web Proxy Autodiscovery (WPAD) URL</help> - </properties> - </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/service_dhcpv6-server.xml.in b/interface-definitions/service_dhcpv6-server.xml.in index 6f7f3c1da..07cbfc85d 100644 --- a/interface-definitions/service_dhcpv6-server.xml.in +++ b/interface-definitions/service_dhcpv6-server.xml.in @@ -9,6 +9,7 @@ </properties> <children> #include <include/generic-disable-node.xml.i> + #include <include/listen-interface-multi-broadcast.xml.i> <node name="global-parameters"> <properties> <help>Additional global parameters for DHCPv6 server</help> @@ -89,11 +90,17 @@ </constraint> </properties> <children> - <node name="address-range"> + #include <include/dhcp/option-v6.xml.i> + <tagNode name="range"> <properties> <help>Parameters setting ranges for assigning IPv6 addresses</help> + <constraint> + #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> + </constraint> + <constraintErrorMessage>Invalid range name, may only be alphanumeric, dot and hyphen</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v6.xml.i> <leafNode name="prefix"> <properties> <help>IPv6 prefix defining range of addresses to assign</help> @@ -104,10 +111,9 @@ <constraint> <validator name="ipv6-prefix"/> </constraint> - <multi/> </properties> </leafNode> - <tagNode name="start"> + <leafNode name="start"> <properties> <help>First in range of consecutive IPv6 addresses to assign</help> <valueHelp> @@ -118,25 +124,21 @@ <validator name="ipv6-address"/> </constraint> </properties> - <children> - <leafNode name="stop"> - <properties> - <help>Last in range of consecutive IPv6 addresses</help> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address</description> - </valueHelp> - <constraint> - <validator name="ipv6-address"/> - </constraint> - </properties> - </leafNode> - </children> - </tagNode> + </leafNode> + <leafNode name="stop"> + <properties> + <help>Last in range of consecutive IPv6 addresses</help> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <constraint> + <validator name="ipv6-address"/> + </constraint> + </properties> + </leafNode> </children> - </node> - #include <include/dhcp/captive-portal.xml.i> - #include <include/dhcp/domain-search.xml.i> + </tagNode> <node name="lease-time"> <properties> <help>Parameters relating to the lease time</help> @@ -180,51 +182,6 @@ </leafNode> </children> </node> - #include <include/name-server-ipv6.xml.i> - <leafNode name="nis-domain"> - <properties> - <help>NIS domain name for client to use</help> - <constraint> - #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> - </constraint> - <constraintErrorMessage>Invalid NIS domain name</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="nis-server"> - <properties> - <help>IPv6 address of a NIS Server</help> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address of NIS server</description> - </valueHelp> - <constraint> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="nisplus-domain"> - <properties> - <help>NIS+ domain name for client to use</help> - <constraint> - #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i> - </constraint> - <constraintErrorMessage>Invalid NIS+ domain name. May only contain letters, numbers and .-_</constraintErrorMessage> - </properties> - </leafNode> - <leafNode name="nisplus-server"> - <properties> - <help>IPv6 address of a NIS+ Server</help> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address of NIS+ server</description> - </valueHelp> - <constraint> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> <node name="prefix-delegation"> <properties> <help>Parameters relating to IPv6 prefix delegation</help> @@ -272,33 +229,6 @@ </tagNode> </children> </node> - <leafNode name="sip-server"> - <properties> - <help>IPv6 address of SIP server</help> - <valueHelp> - <format>ipv6</format> - <description>IPv6 address of SIP server</description> - </valueHelp> - <valueHelp> - <format>hostname</format> - <description>FQDN of SIP server</description> - </valueHelp> - <constraint> - <validator name="ipv6-address"/> - <validator name="fqdn"/> - </constraint> - <multi/> - </properties> - </leafNode> - <leafNode name="sntp-server"> - <properties> - <help>IPv6 address of an SNTP server for client to use</help> - <constraint> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> <tagNode name="static-mapping"> <properties> <help>Hostname for static mapping reservation</help> @@ -308,6 +238,7 @@ <constraintErrorMessage>Invalid static mapping hostname</constraintErrorMessage> </properties> <children> + #include <include/dhcp/option-v6.xml.i> #include <include/generic-disable-node.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/duid.xml.i> @@ -337,33 +268,18 @@ </leafNode> </children> </tagNode> - <node name="vendor-option"> + <leafNode name="subnet-id"> <properties> - <help>Vendor Specific Options</help> + <help>Unique ID mapped to leases in the lease file</help> + <valueHelp> + <format>u32</format> + <description>Unique subnet ID</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-4294967295"/> + </constraint> </properties> - <children> - <node name="cisco"> - <properties> - <help>Cisco specific parameters</help> - </properties> - <children> - <leafNode name="tftp-server"> - <properties> - <help>TFTP server name</help> - <valueHelp> - <format>ipv6</format> - <description>TFTP server IPv6 address</description> - </valueHelp> - <constraint> - <validator name="ipv6-address"/> - </constraint> - <multi/> - </properties> - </leafNode> - </children> - </node> - </children> - </node> + </leafNode> </children> </tagNode> </children> diff --git a/interface-definitions/service_dns_dynamic.xml.in b/interface-definitions/service_dns_dynamic.xml.in index d1b0e90bb..75e5520b7 100644 --- a/interface-definitions/service_dns_dynamic.xml.in +++ b/interface-definitions/service_dns_dynamic.xml.in @@ -38,42 +38,29 @@ </constraint> </properties> </leafNode> - <leafNode name="address"> + <node 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> </properties> <children> - #include <include/url-http-https.xml.i> - <leafNode name="skip"> + #include <include/generic-interface.xml.i> + <node name="web"> <properties> - <help>Pattern to skip from the HTTP(S) respose</help> - <valueHelp> - <format>txt</format> - <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> - </valueHelp> + <help>HTTP(S) web request to use</help> </properties> - </leafNode> + <children> + #include <include/url-http-https.xml.i> + <leafNode name="skip"> + <properties> + <help>Pattern to skip from the HTTP(S) respose</help> + <valueHelp> + <format>txt</format> + <description>Pattern to skip from the HTTP(S) respose to extract the external IP address</description> + </valueHelp> + </properties> + </leafNode> + </children> + </node> </children> </node> <leafNode name="ip-version"> diff --git a/interface-definitions/service_dns_forwarding.xml.in b/interface-definitions/service_dns_forwarding.xml.in index 7dce9b548..a54618e82 100644 --- a/interface-definitions/service_dns_forwarding.xml.in +++ b/interface-definitions/service_dns_forwarding.xml.in @@ -670,6 +670,19 @@ </properties> <defaultValue>3600</defaultValue> </leafNode> + <leafNode name="serve-stale-extension"> + <properties> + <help>Number of times the expired TTL of a record is extended by 30 seconds when serving stale</help> + <valueHelp> + <format>u32:0-65535</format> + <description>Number of times to extend the TTL</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-65535"/> + </constraint> + </properties> + <defaultValue>0</defaultValue> + </leafNode> <leafNode name="timeout"> <properties> <help>Number of milliseconds to wait for a remote authoritative server to respond</help> @@ -694,6 +707,91 @@ <valueless/> </properties> </leafNode> + <leafNode name="exclude-throttle-address"> + <properties> + <help>IP address or subnet</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address to match</description> + </valueHelp> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>ipv6</format> + <description>IPv6 address</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 address</description> + </valueHelp> + <multi/> + <constraint> + <validator name="ipv4-address"/> + <validator name="ipv4-prefix"/> + <validator name="ipv6-address"/> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + </leafNode> + <node name="options"> + <properties> + <help>DNS server options</help> + </properties> + <children> + <leafNode name="ecs-add-for"> + <properties> + <help>Client netmask for which EDNS Client Subnet will be added</help> + <valueHelp> + <format>ipv4net</format> + <description>IPv4 prefix to match</description> + </valueHelp> + <valueHelp> + <format>!ipv4net</format> + <description>Match everything except the specified IPv4 prefix</description> + </valueHelp> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix to match</description> + </valueHelp> + <valueHelp> + <format>!ipv6net</format> + <description>Match everything except the specified IPv6 prefix</description> + </valueHelp> + <constraint> + <validator name="ipv4-prefix"/> + <validator name="ipv4-prefix-exclude"/> + <validator name="ipv6-prefix"/> + <validator name="ipv6-prefix-exclude"/> + </constraint> + <multi/> + </properties> + </leafNode> + <leafNode name="ecs-ipv4-bits"> + <properties> + <help>Number of bits of IPv4 address to pass for EDNS Client Subnet</help> + <valueHelp> + <format>u32:0-32</format> + <description>Number of bits of IPv4 address</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 0-32"/> + </constraint> + </properties> + </leafNode> + <leafNode name="edns-subnet-allow-list"> + <properties> + <help>Netmask or domain that we should enable EDNS subnet for</help> + <valueHelp> + <format>txt</format> + <description>Netmask or domain</description> + </valueHelp> + <multi/> + </properties> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/interface-definitions/service_https.xml.in b/interface-definitions/service_https.xml.in index 223f10962..b60c7ff2e 100644 --- a/interface-definitions/service_https.xml.in +++ b/interface-definitions/service_https.xml.in @@ -8,52 +8,6 @@ <priority>1001</priority> </properties> <children> - <tagNode name="virtual-host"> - <properties> - <help>Identifier for virtual host</help> - <constraint> - <regex>[a-zA-Z0-9-_.:]{1,255}</regex> - </constraint> - <constraintErrorMessage>illegal characters in identifier or identifier longer than 255 characters</constraintErrorMessage> - </properties> - <children> - <leafNode name="listen-address"> - <properties> - <help>Address to listen for HTTPS requests</help> - <completionHelp> - <script>${vyos_completion_dir}/list_local_ips.sh --both</script> - </completionHelp> - <valueHelp> - <format>ipv4</format> - <description>HTTPS IPv4 address</description> - </valueHelp> - <valueHelp> - <format>ipv6</format> - <description>HTTPS IPv6 address</description> - </valueHelp> - <valueHelp> - <format>'*'</format> - <description>any</description> - </valueHelp> - <constraint> - <validator name="ip-address"/> - <regex>\*</regex> - </constraint> - </properties> - </leafNode> - #include <include/port-number.xml.i> - <leafNode name='port'> - <defaultValue>443</defaultValue> - </leafNode> - <leafNode name="server-name"> - <properties> - <help>Server names: exact, wildcard, or regex</help> - <multi/> - </properties> - </leafNode> - #include <include/allow-client.xml.i> - </children> - </tagNode> <node name="api"> <properties> <help>VyOS HTTP API configuration</help> @@ -172,19 +126,18 @@ </node> </children> </node> - <node name="api-restrict"> + #include <include/allow-client.xml.i> + <leafNode name="enable-http-redirect"> <properties> - <help>Restrict api proxy to subset of virtual hosts</help> + <help>Enable HTTP to HTTPS redirect</help> + <valueless/> </properties> - <children> - <leafNode name="virtual-host"> - <properties> - <help>Restrict proxy to virtual host(s)</help> - <multi/> - </properties> - </leafNode> - </children> - </node> + </leafNode> + #include <include/listen-address.xml.i> + #include <include/port-number.xml.i> + <leafNode name='port'> + <defaultValue>443</defaultValue> + </leafNode> <node name="certificates"> <properties> <help>TLS certificates</help> @@ -192,26 +145,30 @@ <children> #include <include/pki/ca-certificate.xml.i> #include <include/pki/certificate.xml.i> - <node name="certbot" owner="${vyos_conf_scripts_dir}/service_https_certificates_certbot.py"> - <properties> - <help>Request or apply a letsencrypt certificate for domain-name</help> - </properties> - <children> - <leafNode name="domain-name"> - <properties> - <help>Domain name(s) for which to obtain certificate</help> - <multi/> - </properties> - </leafNode> - <leafNode name="email"> - <properties> - <help>Email address to associate with certificate</help> - </properties> - </leafNode> - </children> - </node> + #include <include/pki/dh-params.xml.i> </children> </node> + <leafNode name="tls-version"> + <properties> + <help>Specify available TLS version(s)</help> + <completionHelp> + <list>1.2 1.3</list> + </completionHelp> + <valueHelp> + <format>1.2</format> + <description>TLSv1.2</description> + </valueHelp> + <valueHelp> + <format>1.3</format> + <description>TLSv1.3</description> + </valueHelp> + <constraint> + <regex>(1.2|1.3)</regex> + </constraint> + <multi/> + </properties> + <defaultValue>1.2 1.3</defaultValue> + </leafNode> #include <include/interface/vrf.xml.i> </children> </node> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index edfe6a34c..eeec2aeef 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -183,6 +183,7 @@ </children> </node> #include <include/accel-ppp/default-pool.xml.i> + #include <include/accel-ppp/default-ipv6-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/service_ndp-proxy.xml.in b/interface-definitions/service_ndp-proxy.xml.in index 9801c99ab..aabba3f4e 100644 --- a/interface-definitions/service_ndp-proxy.xml.in +++ b/interface-definitions/service_ndp-proxy.xml.in @@ -5,6 +5,7 @@ <node name="ndp-proxy" owner="${vyos_conf_scripts_dir}/service_ndp-proxy.py"> <properties> <help>Neighbor Discovery Protocol (NDP) Proxy</help> + <priority>600</priority> </properties> <children> <leafNode name="route-refresh"> diff --git a/interface-definitions/service_ntp.xml.in b/interface-definitions/service_ntp.xml.in index 65a45d7a1..c057b62b5 100644 --- a/interface-definitions/service_ntp.xml.in +++ b/interface-definitions/service_ntp.xml.in @@ -9,6 +9,38 @@ <priority>900</priority> </properties> <children> + #include <include/allow-client.xml.i> + #include <include/generic-interface.xml.i> + #include <include/listen-address.xml.i> + #include <include/interface/vrf.xml.i> + <leafNode name="leap-second"> + <properties> + <help>Leap second behavior</help> + <completionHelp> + <list>ignore smear system timezone</list> + </completionHelp> + <valueHelp> + <format>ignore</format> + <description>No correction is applied to the clock for the leap second</description> + </valueHelp> + <valueHelp> + <format>smear</format> + <description>Correct served time slowly be slewing instead of stepping</description> + </valueHelp> + <valueHelp> + <format>system</format> + <description>Kernel steps the system clock forward or backward</description> + </valueHelp> + <valueHelp> + <format>timezone</format> + <description>Use UTC timezone database to determine when will the next leap second occur</description> + </valueHelp> + <constraint> + <regex>(ignore|smear|system|timezone)</regex> + </constraint> + </properties> + <defaultValue>timezone</defaultValue> + </leafNode> <tagNode name="server"> <properties> <help>Network Time Protocol (NTP) server</help> @@ -56,10 +88,6 @@ </leafNode> </children> </tagNode> - #include <include/allow-client.xml.i> - #include <include/generic-interface.xml.i> - #include <include/listen-address.xml.i> - #include <include/interface/vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/service_pppoe-server.xml.in b/interface-definitions/service_pppoe-server.xml.in index f1b369936..6fdc2a65a 100644 --- a/interface-definitions/service_pppoe-server.xml.in +++ b/interface-definitions/service_pppoe-server.xml.in @@ -274,6 +274,7 @@ </children> </node> #include <include/accel-ppp/default-pool.xml.i> + #include <include/accel-ppp/default-ipv6-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/system_config-management.xml.in b/interface-definitions/system_config-management.xml.in index 7ae347955..e666633b7 100644 --- a/interface-definitions/system_config-management.xml.in +++ b/interface-definitions/system_config-management.xml.in @@ -51,15 +51,7 @@ <multi/> </properties> </leafNode> - <leafNode name="source-address"> - <properties> - <help>Source address or interface for archive server connections</help> - <constraint> - <validator name="ip-address"/> - #include <include/constraint/interface-name.xml.i> - </constraint> - </properties> - </leafNode> + #include <include/source-address-ipv4-ipv6.xml.i> </children> </node> <leafNode name="commit-revisions"> diff --git a/interface-definitions/system_option.xml.in b/interface-definitions/system_option.xml.in index adb45bdcc..602d7d100 100644 --- a/interface-definitions/system_option.xml.in +++ b/interface-definitions/system_option.xml.in @@ -32,6 +32,19 @@ <constraintErrorMessage>Must be ignore, reboot, or poweroff</constraintErrorMessage> </properties> </leafNode> + <node name="kernel"> + <properties> + <help>Kernel boot parameters</help> + </properties> + <children> + <leafNode name="disable-mitigations"> + <properties> + <help>Disable all optional CPU mitigations</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> <leafNode name="keyboard-layout"> <properties> <help>System keyboard layout, type ISO2</help> diff --git a/interface-definitions/system_sflow.xml.in b/interface-definitions/system_sflow.xml.in index c5152abe9..aaf4033d8 100644 --- a/interface-definitions/system_sflow.xml.in +++ b/interface-definitions/system_sflow.xml.in @@ -106,6 +106,7 @@ </leafNode> </children> </tagNode> + #include <include/interface/vrf.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in index 1847401b5..9d1d5d824 100644 --- a/interface-definitions/vpn_ipsec.xml.in +++ b/interface-definitions/vpn_ipsec.xml.in @@ -251,22 +251,22 @@ <properties> <help>Action to take if a child SA is unexpectedly closed</help> <completionHelp> - <list>none hold restart</list> + <list>none trap start</list> </completionHelp> <valueHelp> <format>none</format> <description>Do nothing</description> </valueHelp> <valueHelp> - <format>hold</format> + <format>trap</format> <description>Attempt to re-negotiate when matching traffic is seen</description> </valueHelp> <valueHelp> - <format>restart</format> + <format>start</format> <description>Attempt to re-negotiate the connection immediately</description> </valueHelp> <constraint> - <regex>(none|hold|restart)</regex> + <regex>(none|trap|start)</regex> </constraint> </properties> <defaultValue>none</defaultValue> @@ -280,10 +280,10 @@ <properties> <help>Keep-alive failure action</help> <completionHelp> - <list>hold clear restart</list> + <list>trap clear restart</list> </completionHelp> <valueHelp> - <format>hold</format> + <format>trap</format> <description>Attempt to re-negotiate the connection when matching traffic is seen</description> </valueHelp> <valueHelp> @@ -295,7 +295,7 @@ <description>Attempt to re-negotiate the connection immediately</description> </valueHelp> <constraint> - <regex>(hold|clear|restart)</regex> + <regex>(trap|clear|restart)</regex> </constraint> </properties> <defaultValue>clear</defaultValue> diff --git a/interface-definitions/vpn_l2tp.xml.in b/interface-definitions/vpn_l2tp.xml.in index 3e2d00e6b..d3fb58433 100644 --- a/interface-definitions/vpn_l2tp.xml.in +++ b/interface-definitions/vpn_l2tp.xml.in @@ -154,6 +154,7 @@ </children> </node> #include <include/accel-ppp/default-pool.xml.i> + #include <include/accel-ppp/default-ipv6-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_pptp.xml.in b/interface-definitions/vpn_pptp.xml.in index 7bb8db798..ec622b5d0 100644 --- a/interface-definitions/vpn_pptp.xml.in +++ b/interface-definitions/vpn_pptp.xml.in @@ -134,6 +134,7 @@ </children> </node> #include <include/accel-ppp/default-pool.xml.i> + #include <include/accel-ppp/default-ipv6-pool.xml.i> </children> </node> </children> diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in index a1b69f990..2727540be 100644 --- a/interface-definitions/vpn_sstp.xml.in +++ b/interface-definitions/vpn_sstp.xml.in @@ -36,6 +36,7 @@ <defaultValue>443</defaultValue> </leafNode> #include <include/accel-ppp/default-pool.xml.i> + #include <include/accel-ppp/default-ipv6-pool.xml.i> <node name="ppp-options"> <properties> <help>PPP (Point-to-Point Protocol) settings</help> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index f581d39fa..96c582a83 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -154,6 +154,9 @@ </children> </node> <node name="update"> + <properties> + <help>Update data for a service</help> + </properties> <children> <node name="container"> <properties> diff --git a/op-mode-definitions/dns-dynamic.xml.in b/op-mode-definitions/dns-dynamic.xml.in index 79478f392..45d58e2e8 100644 --- a/op-mode-definitions/dns-dynamic.xml.in +++ b/op-mode-definitions/dns-dynamic.xml.in @@ -4,7 +4,7 @@ <children> <node name="dns"> <properties> - <help>Clear Domain Name System</help> + <help>Clear Domain Name System (DNS) related service state</help> </properties> <children> <node name="dynamic"> @@ -30,7 +30,7 @@ <children> <node name="dns"> <properties> - <help>Monitor last lines of Domain Name System related services</help> + <help>Monitor last lines of Domain Name System (DNS) related services</help> </properties> <children> <node name="dynamic"> @@ -51,7 +51,7 @@ <children> <node name="dns"> <properties> - <help>Show log for Domain Name System related services</help> + <help>Show log for Domain Name System (DNS) related services</help> </properties> <children> <node name="dynamic"> @@ -66,7 +66,7 @@ </node> <node name="dns"> <properties> - <help>Show Domain Name System related information</help> + <help>Show Domain Name System (DNS) related information</help> </properties> <children> <node name="dynamic"> @@ -78,7 +78,7 @@ <properties> <help>Show Dynamic DNS status</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --status</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py show_dynamic_status</command> </leafNode> </children> </node> @@ -90,34 +90,31 @@ <children> <node name="dns"> <properties> - <help>Restart specific Domain Name System related service</help> + <help>Restart specific Domain Name System (DNS) related service</help> </properties> <children> <node name="dynamic"> <properties> <help>Restart Dynamic DNS service</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> + <command>if cli-shell-api existsActive service dns dynamic; then sudo systemctl restart ddclient.service; else echo "Dynamic DNS not configured"; fi</command> </node> </children> </node> </children> </node> - <node name="update"> - <properties> - <help>Update data for a service</help> - </properties> + <node name="reset"> <children> <node name="dns"> <properties> - <help>Update Domain Name System related information</help> + <help>Reset Domain Name System (DNS) related service state</help> </properties> <children> <node name="dynamic"> <properties> - <help>Update Dynamic DNS information</help> + <help>Reset Dynamic DNS information</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_dynamic.py --update</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py reset_dynamic</command> </node> </children> </node> diff --git a/op-mode-definitions/dns-forwarding.xml.in b/op-mode-definitions/dns-forwarding.xml.in index a4c650c38..29bfc61cf 100644 --- a/op-mode-definitions/dns-forwarding.xml.in +++ b/op-mode-definitions/dns-forwarding.xml.in @@ -11,7 +11,7 @@ <children> <node name="forwarding"> <properties> - <help>Monitor last lines of DNS forwarding</help> + <help>Monitor last lines of DNS Forwarding service</help> </properties> <command>journalctl --no-hostname --follow --boot --unit pdns-recursor.service</command> </node> @@ -47,12 +47,12 @@ <children> <node name="forwarding"> <properties> - <help>Show DNS forwarding information</help> + <help>Show DNS Forwarding information</help> </properties> <children> <leafNode name="statistics"> <properties> - <help>Show DNS forwarding statistics</help> + <help>Show DNS Forwarding statistics</help> </properties> <command>sudo ${vyos_op_scripts_dir}/dns.py show_forwarding_statistics</command> </leafNode> @@ -71,9 +71,9 @@ <children> <leafNode name="forwarding"> <properties> - <help>Restart DNS forwarding service</help> + <help>Restart DNS Forwarding service</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_restart.sh</command> + <command>if cli-shell-api existsActive service dns forwarding; then sudo systemctl restart pdns-recursor.service; else echo "DNS forwarding not configured"; fi</command> </leafNode> </children> </node> @@ -88,19 +88,19 @@ <children> <node name="forwarding"> <properties> - <help>Reset DNS forwarding cache</help> + <help>Reset DNS Forwarding cache</help> </properties> <children> <tagNode name="domain"> - <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py $5</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py reset_forwarding --domain $5</command> <properties> - <help>Reset DNS forwarding cache for a domain</help> + <help>Reset DNS Forwarding cache for a domain</help> </properties> </tagNode> <leafNode name="all"> - <command>sudo ${vyos_op_scripts_dir}/dns_forwarding_reset.py --all</command> + <command>sudo ${vyos_op_scripts_dir}/dns.py reset_forwarding --all</command> <properties> - <help>Reset DNS forwarding cache</help> + <help>Reset DNS Forwarding cache for all domains</help> </properties> </leafNode> </children> diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in index 4a7ffbb66..50d52d6ca 100644 --- a/op-mode-definitions/firewall.xml.in +++ b/op-mode-definitions/firewall.xml.in @@ -1,110 +1,5 @@ <?xml version="1.0"?> <interfaceDefinition> -<!-- - <node name="clear"> - <children> - <node name="firewall"> - <properties> - <help>Clear firewall statistics</help> - </properties> - <children> - <tagNode name="ipv6-name"> - <properties> - <help>Clear firewall statistics for chain</help> - <completionHelp> - <path>firewall ipv6-name</path> - </completionHelp> - </properties> - <children> - <leafNode name="counters"> - <properties> - <help>Clear counters for specified chain</help> - </properties> - <command>echo "TODO"</command> - </leafNode> - <tagNode name="rule"> - <properties> - <help>Clear firewall statistics for a rule</help> - <completionHelp> - <path>firewall ipv6-name ${COMP_WORDS[4]} rule</path> - </completionHelp> - </properties> - <children> - <leafNode name="counters"> - <properties> - <help>Clear counters for specified rule</help> - </properties> - <command>echo "TODO"</command> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - <tagNode name="name"> - <properties> - <help>Clear firewall statistics for chain</help> - <completionHelp> - <path>firewall name</path> - </completionHelp> - </properties> - <children> - <leafNode name="counters"> - <properties> - <help>Clear counters for specified chain</help> - </properties> - <command>echo "TODO"</command> - </leafNode> - <tagNode name="rule"> - <properties> - <help>Clear firewall statistics for a rule</help> - <completionHelp> - <path>firewall name ${COMP_WORDS[4]} rule</path> - </completionHelp> - </properties> - <children> - <leafNode name="counters"> - <properties> - <help>Clear counters for specified rule</help> - </properties> - <command>echo "TODO"</command> - </leafNode> - </children> - </tagNode> - </children> - </tagNode> - </children> - </node> - </children> - </node> ---> -<!-- - <node name="reset"> - <children> - <node name="firewall"> - <properties> - <help>Reset a firewall group</help> - </properties> - <children> - <tagNode name="address-group"> - <properties> - <help>Reset a firewall address group</help> - </properties> - </tagNode> - <tagNode name="network-group"> - <properties> - <help>Reset a firewall network group</help> - </properties> - </tagNode> - <tagNode name="port-group"> - <properties> - <help>Reset a firewall port group</help> - </properties> - </tagNode> - </children> - </node> - </children> - </node> ---> <node name="show"> <children> <node name="firewall"> @@ -173,7 +68,7 @@ <properties> <help>Show summary of bridge custom firewall ruleset</help> <completionHelp> - <path>firewall bridge name ${COMP_WORDS[6]} rule</path> + <path>firewall bridge name ${COMP_WORDS[5]} rule</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7</command> @@ -261,11 +156,11 @@ </node> </children> </node> - <tagNode name="ipv6-name"> + <tagNode name="name"> <properties> <help>Show IPv6 custom firewall chains</help> <completionHelp> - <path>firewall ipv6 ipv6-name</path> + <path>firewall ipv6 name</path> </completionHelp> </properties> <children> @@ -273,7 +168,7 @@ <properties> <help>Show summary of IPv6 custom firewall ruleset</help> <completionHelp> - <path>firewall ipv6 ipv6-name ${COMP_WORDS[6]} rule</path> + <path>firewall ipv6 name ${COMP_WORDS[5]} rule</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7</command> @@ -373,7 +268,7 @@ <properties> <help>Show summary of IPv4 custom firewall ruleset</help> <completionHelp> - <path>firewall ipv4 name ${COMP_WORDS[6]} rule</path> + <path>firewall ipv4 name ${COMP_WORDS[5]} rule</path> </completionHelp> </properties> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show --family $3 --hook $4 --priority $5 --rule $7</command> @@ -396,6 +291,23 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_summary</command> </leafNode> + <node name="zone-policy"> + <properties> + <help>Show zone policy information</help> + </properties> + <children> + <tagNode name="zone"> + <properties> + <help>Show summary of zone policy for a specific zone</help> + <completionHelp> + <path>firewall zone</path> + </completionHelp> + </properties> + <command>sudo ${vyos_op_scripts_dir}/zone.py show --zone $5</command> + </tagNode> + </children> + <command>sudo ${vyos_op_scripts_dir}/zone.py show</command> + </node> </children> <command>sudo ${vyos_op_scripts_dir}/firewall.py --action show_all</command> </node> diff --git a/op-mode-definitions/generate-system-login-user.xml.in b/op-mode-definitions/generate-system-login-user.xml.in index 6f65c12b3..6f65c12b3 100755..100644 --- a/op-mode-definitions/generate-system-login-user.xml.in +++ b/op-mode-definitions/generate-system-login-user.xml.in diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in index c03ec4cce..559952e25 100644 --- a/op-mode-definitions/monitor-log.xml.in +++ b/op-mode-definitions/monitor-log.xml.in @@ -30,6 +30,12 @@ </leafNode> </children> </node> + <leafNode name="certbot"> + <properties> + <help>Monitor last lines of certbot log</help> + </properties> + <command>if sudo test -f /var/log/letsencrypt/letsencrypt.log; then sudo tail --follow=name /var/log/letsencrypt/letsencrypt.log; else echo "Cerbot log does not exist"; fi</command> + </leafNode> <leafNode name="conntrack-sync"> <properties> <help>Monitor last lines of conntrack-sync log</help> diff --git a/op-mode-definitions/multicast-group.xml.in b/op-mode-definitions/multicast-group.xml.in new file mode 100644 index 000000000..39b4e347c --- /dev/null +++ b/op-mode-definitions/multicast-group.xml.in @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="show"> + <children> + <node name="ip"> + <children> + <node name="multicast"> + <properties> + <help>Show IP multicast</help> + </properties> + <children> + <node name="group"> + <properties> + <help>Show IP multicast group membership</help> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show IP multicast group membership of specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet --interface "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + <node name="ipv6"> + <children> + <node name="multicast"> + <properties> + <help>Show IPv6 multicast</help> + </properties> + <children> + <node name="group"> + <properties> + <help>Show IPv6 multicast group membership</help> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet6</command> + <children> + <tagNode name="interface"> + <properties> + <help>Show IP multicast group membership of specific interface</help> + <completionHelp> + <script>${vyos_completion_dir}/list_interfaces</script> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/multicast.py show_group --family inet6 --interface "$6"</command> + </tagNode> + </children> + </node> + </children> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index ca0eb3687..4b8d9c47a 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -574,4 +574,14 @@ </node> </children> </node> + <node name="renew"> + <children> + <leafNode name="certbot"> + <properties> + <help>Start manual certbot renewal</help> + </properties> + <command>sudo systemctl start certbot.service</command> + </leafNode> + </children> + </node> </interfaceDefinition> diff --git a/op-mode-definitions/rpki.xml.in b/op-mode-definitions/rpki.xml.in index 72d378b88..9e0f83e20 100644 --- a/op-mode-definitions/rpki.xml.in +++ b/op-mode-definitions/rpki.xml.in @@ -7,6 +7,15 @@ <help>Show RPKI (Resource Public Key Infrastructure) information</help> </properties> <children> + <tagNode name="as-number"> + <properties> + <help>Lookup by ASN in prefix table</help> + <completionHelp> + <list><ASNUM></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </tagNode> <leafNode name="cache-connection"> <properties> <help>Show RPKI cache connections</help> @@ -19,6 +28,26 @@ </properties> <command>vtysh -c "show rpki cache-server"</command> </leafNode> + <tagNode name="prefix"> + <properties> + <help>Lookup IP prefix and optionally ASN in prefix table</help> + <completionHelp> + <list><x.x.x.x/x> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <tagNode name="as-number"> + <properties> + <help>AS Number</help> + <completionHelp> + <list><ASNUM></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $(echo $@ | sed -e "s/as-number //g")</command> + </tagNode> + </children> + </tagNode> <leafNode name="prefix-table"> <properties> <help>Show RPKI-validated prefixes</help> diff --git a/op-mode-definitions/show-ip-multicast.xml.in b/op-mode-definitions/show-ip-multicast.xml.in index 605d61e8d..00a4704c7 100644 --- a/op-mode-definitions/show-ip-multicast.xml.in +++ b/op-mode-definitions/show-ip-multicast.xml.in @@ -5,9 +5,6 @@ <node name="ip"> <children> <node name="multicast"> - <properties> - <help>Show IP multicast</help> - </properties> <children> <leafNode name="interface"> <properties> diff --git a/op-mode-definitions/show-ipv6-route.xml.in b/op-mode-definitions/show-ipv6-route.xml.in index 7df1a873a..d73fb46b4 100644 --- a/op-mode-definitions/show-ipv6-route.xml.in +++ b/op-mode-definitions/show-ipv6-route.xml.in @@ -82,6 +82,23 @@ </properties> <command>${vyos_op_scripts_dir}/route.py show_summary --family inet6 --vrf $5</command> </node> + <node name="node.tag"> + <properties> + <help>Show IPv6 routes of given address or prefix</help> + <completionHelp> + <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> + </completionHelp> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + <children> + <node name="longer-prefixes"> + <properties> + <help>Show longer prefixes of routes for given prefix</help> + </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> + </node> + </children> + </node> #include <include/show-route-bgp.xml.i> #include <include/show-route-connected.xml.i> #include <include/show-route-isis.xml.i> @@ -103,6 +120,7 @@ <list><h:h:h:h:h:h:h:h> <h:h:h:h:h:h:h:h/x></list> </completionHelp> </properties> + <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> <children> <node name="longer-prefixes"> <properties> @@ -111,7 +129,6 @@ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command> </node> </children> - <command>vtysh -c "show ipv6 route $4"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index b013bdfe4..a6ce04624 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -38,6 +38,12 @@ </properties> <command>journalctl --no-hostname --boot --quiet SYSLOG_FACILITY=10 SYSLOG_FACILITY=4</command> </leafNode> + <leafNode name="certbot"> + <properties> + <help>Show log for certbot</help> + </properties> + <command>if sudo test -f /var/log/letsencrypt/letsencrypt.log; then sudo cat /var/log/letsencrypt/letsencrypt.log; else echo "Cerbot log does not exist"; fi</command> + </leafNode> <leafNode name="cluster"> <properties> <help>Show log for Cluster</help> diff --git a/python/vyos/accel_ppp_util.py b/python/vyos/accel_ppp_util.py index 757d447a2..2f029e042 100644 --- a/python/vyos/accel_ppp_util.py +++ b/python/vyos/accel_ppp_util.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 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 @@ -22,9 +22,9 @@ # makes use of it! from vyos import ConfigError +from vyos.base import Warning 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 @@ -156,38 +156,47 @@ def verify_accel_ppp_base_service(config, local_users=True): "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 + and client-ipv6-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') + 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!") + Warning("IPv4 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') + + if 'client_ipv6_pool' in vpn_config: + for ipv6_pool, ipv6_pool_config in vpn_config['client_ipv6_pool'].items(): + if 'delegate' in ipv6_pool_config and 'prefix' not in ipv6_pool_config: + raise ConfigError( + f'IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured in "{ipv6_pool}"!') + + if dict_search('authentication.mode', vpn_config) in ['local', 'noauth']: + if not dict_search('client_ip_pool', vpn_config) and not dict_search( + 'client_ipv6_pool', vpn_config): + raise ConfigError( + "L2TP local auth mode requires local client-ip-pool or client-ipv6-pool to be configured!") + if dict_search('client_ip_pool', vpn_config) and not dict_search( + 'default_pool', vpn_config): + Warning("'default-pool' is not defined") + if dict_search('client_ipv6_pool', vpn_config) and not dict_search( + 'default_ipv6_pool', vpn_config): + Warning("'default-ipv6-pool' is not defined") diff --git a/python/vyos/config.py b/python/vyos/config.py index ca7b035e5..bee85315d 100644 --- a/python/vyos/config.py +++ b/python/vyos/config.py @@ -92,6 +92,38 @@ def config_dict_merge(src: dict, dest: Union[dict, ConfigDict]) -> ConfigDict: dest = ConfigDict(dest) return ext_dict_merge(src, dest) +def config_dict_mangle_acme(name, cli_dict): + """ + Load CLI PKI dictionary and if an ACME certificate is used, load it's content + and place it into the CLI dictionary as it would be a "regular" CLI PKI based + certificate with private key + """ + from vyos.base import ConfigError + from vyos.defaults import directories + from vyos.utils.file import read_file + from vyos.pki import encode_certificate + from vyos.pki import encode_private_key + from vyos.pki import load_certificate + from vyos.pki import load_private_key + + try: + vyos_certbot_dir = directories['certbot'] + + if 'acme' in cli_dict: + tmp = read_file(f'{vyos_certbot_dir}/live/{name}/cert.pem') + tmp = load_certificate(tmp, wrap_tags=False) + cert_base64 = "".join(encode_certificate(tmp).strip().split("\n")[1:-1]) + + tmp = read_file(f'{vyos_certbot_dir}/live/{name}/privkey.pem') + tmp = load_private_key(tmp, wrap_tags=False) + key_base64 = "".join(encode_private_key(tmp).strip().split("\n")[1:-1]) + # install ACME based PEM keys into "regular" CLI config keys + cli_dict.update({'certificate' : cert_base64, 'private' : {'key' : key_base64}}) + except: + raise ConfigError(f'Unable to load ACME certificates for "{name}"!') + + return cli_dict + class Config(object): """ The class of config access objects. @@ -306,6 +338,11 @@ class Config(object): no_tag_node_value_mangle=True, get_first_key=True) if pki_dict: + if 'certificate' in pki_dict: + for certificate in pki_dict['certificate']: + pki_dict['certificate'][certificate] = config_dict_mangle_acme( + certificate, pki_dict['certificate'][certificate]) + conf_dict['pki'] = pki_dict # save optional args for a call to get_config_defaults diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 85423142d..5d3723876 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -1,4 +1,4 @@ -# Copyright 2020-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2020-2024 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 @@ -25,6 +25,9 @@ from vyos import ConfigError from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_recursive +# pattern re-used in ipsec migration script +dynamic_interface_pattern = r'(ppp|pppoe|sstpc|l2tp|ipoe)[0-9]+' + def verify_mtu(config): """ Common helper function used by interface implementations to perform @@ -290,7 +293,7 @@ def verify_source_interface(config): src_ifname = config['source_interface'] # We do not allow sourcing other interfaces (e.g. tunnel) from dynamic interfaces - tmp = re.compile(r'(ppp|pppoe|sstpc|l2tp|ipoe)[0-9]+') + tmp = re.compile(dynamic_interface_pattern) if tmp.match(src_ifname): raise ConfigError(f'Can not source "{ifname}" from dynamic interface "{src_ifname}"!') diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 2f3580571..64145a42e 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -37,6 +37,7 @@ directories = { } config_status = '/tmp/vyos-config-status' +api_config_state = '/run/http-api-state' cfg_group = 'vyattacfg' @@ -45,14 +46,3 @@ cfg_vintage = 'vyos' commit_lock = '/opt/vyatta/config/.lock' component_version_json = os.path.join(directories['data'], 'component-versions.json') - -https_data = { - 'listen_addresses' : { '*': ['_'] } -} - -vyos_cert_data = { - 'conf' : '/etc/nginx/snippets/vyos-cert.conf', - 'crt' : '/etc/ssl/certs/vyos-selfsigned.crt', - 'key' : '/etc/ssl/private/vyos-selfsign', - 'lifetime' : '365', -} diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py index 1b977b16e..eee11bd2d 100644 --- a/python/vyos/firewall.py +++ b/python/vyos/firewall.py @@ -288,7 +288,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): operator = '!=' iiface = iiface[1:] output.append(f'iifname {operator} {{{iiface}}}') - else: + elif 'group' in rule_conf['inbound_interface']: iiface = rule_conf['inbound_interface']['group'] if iiface[0] == '!': operator = '!=' @@ -303,7 +303,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name): operator = '!=' oiface = oiface[1:] output.append(f'oifname {operator} {{{oiface}}}') - else: + elif 'group' in rule_conf['outbound_interface']: oiface = rule_conf['outbound_interface']['group'] if oiface[0] == '!': operator = '!=' diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index aaf903acd..c3f5bbf47 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -19,6 +19,7 @@ from glob import glob from vyos.base import Warning from vyos.ethtool import Ethtool +from vyos.ifconfig import Section from vyos.ifconfig.interface import Interface from vyos.utils.dict import dict_search from vyos.utils.file import read_file @@ -128,6 +129,10 @@ class EthernetIf(Interface): # will remain visible for the operating system. self.set_admin_state('down') + # Remove all VLAN subinterfaces - filter with the VLAN dot + for vlan in [x for x in Section.interfaces(self.iftype) if x.startswith(f'{self.ifname}.')]: + Interface(vlan).remove() + super().remove() def set_flow_control(self, enable): @@ -447,7 +452,7 @@ class EthernetIf(Interface): self.set_gso(dict_search('offload.gso', config) != None) # GSO (generic segmentation offload) - self.set_hw_tc_offload(dict_search('offload.hw-tc-offload', config) != None) + self.set_hw_tc_offload(dict_search('offload.hw_tc_offload', config) != None) # LRO (large receive offload) self.set_lro(dict_search('offload.lro', config) != None) diff --git a/python/vyos/kea.py b/python/vyos/kea.py index 819fe16a9..fb5afc2ce 100644 --- a/python/vyos/kea.py +++ b/python/vyos/kea.py @@ -25,7 +25,7 @@ from vyos.template import netmask_from_cidr from vyos.utils.dict import dict_search_args from vyos.utils.file import file_permissions from vyos.utils.file import read_file -from vyos.utils.process import cmd +from vyos.utils.process import run kea4_options = { 'name_server': 'domain-name-servers', @@ -92,17 +92,28 @@ def kea_parse_options(config): options.append({'name': 'pcode', 'data': tz_string}) options.append({'name': 'tcode', 'data': config['time_zone']}) + unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') + if unifi_controller: + options.append({ + 'name': 'unifi-controller', + 'data': unifi_controller, + 'space': 'ubnt' + }) + return options def kea_parse_subnet(subnet, config): - out = {'subnet': subnet} - options = kea_parse_options(config) + out = {'subnet': subnet, 'id': int(config['subnet_id'])} + options = [] + + if 'option' in config: + out['option-data'] = kea_parse_options(config['option']) - if 'bootfile_name' in config: - out['boot-file-name'] = config['bootfile_name'] + if 'bootfile_name' in config['option']: + out['boot-file-name'] = config['option']['bootfile_name'] - if 'bootfile_server' in config: - out['next-server'] = config['bootfile_server'] + if 'bootfile_server' in config['option']: + out['next-server'] = config['option']['bootfile_server'] if 'lease' in config: out['valid-lifetime'] = int(config['lease']) @@ -112,7 +123,20 @@ def kea_parse_subnet(subnet, config): pools = [] for num, range_config in config['range'].items(): start, stop = range_config['start'], range_config['stop'] - pools.append({'pool': f'{start} - {stop}'}) + pool = { + 'pool': f'{start} - {stop}' + } + + if 'option' in range_config: + pool['option-data'] = kea_parse_options(range_config['option']) + + if 'bootfile_name' in range_config['option']: + pool['boot-file-name'] = range_config['option']['bootfile_name'] + + if 'bootfile_server' in range_config['option']: + pool['next-server'] = range_config['option']['bootfile_server'] + + pools.append(pool) out['pools'] = pools if 'static_mapping' in config: @@ -134,35 +158,23 @@ def kea_parse_subnet(subnet, config): if 'ip_address' in host_config: reservation['ip-address'] = host_config['ip_address'] - reservations.append(reservation) - out['reservations'] = reservations + if 'option' in host_config: + reservation['option-data'] = kea_parse_options(host_config['option']) - unifi_controller = dict_search_args(config, 'vendor_option', 'ubiquiti', 'unifi_controller') - if unifi_controller: - options.append({ - 'name': 'unifi-controller', - 'data': unifi_controller, - 'space': 'ubnt' - }) + if 'bootfile_name' in host_config['option']: + reservation['boot-file-name'] = host_config['option']['bootfile_name'] - if options: - out['option-data'] = options + if 'bootfile_server' in host_config['option']: + reservation['next-server'] = host_config['option']['bootfile_server'] + + reservations.append(reservation) + out['reservations'] = reservations return out def kea6_parse_options(config): options = [] - if 'common_options' in config: - common_opt = config['common_options'] - - for node, option_name in kea6_options.items(): - if node not in common_opt: - continue - - value = ", ".join(common_opt[node]) if isinstance(common_opt[node], list) else common_opt[node] - options.append({'name': option_name, 'data': value}) - for node, option_name in kea6_options.items(): if node not in config: continue @@ -195,21 +207,28 @@ def kea6_parse_options(config): return options def kea6_parse_subnet(subnet, config): - out = {'subnet': subnet} - options = kea6_parse_options(config) + out = {'subnet': subnet, 'id': int(config['subnet_id'])} + + if 'option' in config: + out['option-data'] = kea6_parse_options(config['option']) - if 'address_range' in config: - addr_range = config['address_range'] + if 'range' in config: pools = [] + for num, range_config in config['range'].items(): + pool = {} - if 'prefix' in addr_range: - for prefix in addr_range['prefix']: - pools.append({'pool': prefix}) + if 'prefix' in range_config: + pool['pool'] = range_config['prefix'] - if 'start' in addr_range: - for start, range_conf in addr_range['start'].items(): - stop = range_conf['stop'] - pools.append({'pool': f'{start} - {stop}'}) + if 'start' in range_config: + start = range_config['start'] + stop = range_config['stop'] + pool['pool'] = f'{start} - {stop}' + + if 'option' in range_config: + pool['option-data'] = kea6_parse_options(range_config['option']) + + pools.append(pool) out['pools'] = pools @@ -256,13 +275,13 @@ def kea6_parse_subnet(subnet, config): if 'ipv6_prefix' in host_config: reservation['prefixes'] = [ host_config['ipv6_prefix'] ] + if 'option' in host_config: + reservation['option-data'] = kea6_parse_options(host_config['option']) + reservations.append(reservation) out['reservations'] = reservations - if options: - out['option-data'] = options - return out def kea_parse_leases(lease_path): @@ -293,7 +312,7 @@ def _ctrl_socket_command(path, command, args=None): return None if file_permissions(path) != '0775': - cmd(f'sudo chmod 775 {path}') + run(f'sudo chmod 775 {path}') with socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) as sock: sock.connect(path) diff --git a/python/vyos/nat.py b/python/vyos/nat.py index 392d38772..7215aac88 100644 --- a/python/vyos/nat.py +++ b/python/vyos/nat.py @@ -89,7 +89,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): if addr and is_ip_network(addr): if not ipv6: map_addr = dict_search_args(rule_conf, nat_type, 'address') - translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}') + if port: + translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} . {port} }}') + else: + translation_output.append(f'{ip_prefix} prefix to {ip_prefix} {translation_prefix}addr map {{ {map_addr} : {addr} }}') ignore_type_addr = True else: translation_output.append(f'prefix to {addr}') @@ -112,7 +115,10 @@ def parse_nat_rule(rule_conf, rule_id, nat_type, ipv6=False): if port_mapping and port_mapping != 'none': options.append(port_mapping) - translation_str = " ".join(translation_output) + (f':{port}' if port else '') + if ((not addr) or (addr and not is_ip_network(addr))) and port: + translation_str = " ".join(translation_output) + (f':{port}') + else: + translation_str = " ".join(translation_output) if options: translation_str += f' {",".join(options)}' diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index 230a85541..e1af1a682 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2024 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 @@ -81,7 +81,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|delete|generate|set)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name): return True else: return False @@ -275,4 +275,3 @@ def run(module): # Other functions should not return anything, # although they may print their own warnings or status messages func(**args) - diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index d8bbfe970..a22039e52 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2024 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 @@ -14,6 +14,7 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. import os +import jmespath from vyos.base import Warning from vyos.utils.process import cmd @@ -166,14 +167,17 @@ class QoSBase: } if rate == 'auto' or rate.endswith('%'): - speed = 10 + speed = 1000 + default_speed = speed # Not all interfaces have valid entries in the speed file. PPPoE # interfaces have the appropriate speed file, but you can not read it: # cat: /sys/class/net/pppoe7/speed: Invalid argument try: speed = read_file(f'/sys/class/net/{self._interface}/speed') if not speed.isnumeric(): - Warning('Interface speed cannot be determined (assuming 10 Mbit/s)') + Warning('Interface speed cannot be determined (assuming 1000 Mbit/s)') + if int(speed) < 1: + speed = default_speed if rate.endswith('%'): percent = rate.rstrip('%') speed = int(speed) * int(percent) // 100 @@ -223,6 +227,9 @@ class QoSBase: if 'mark' in match_config: mark = match_config['mark'] filter_cmd += f' handle {mark} fw' + if 'vif' in match_config: + vif = match_config['vif'] + filter_cmd += f' basic match "meta(vlan mask 0xfff eq {vif})"' for af in ['ip', 'ipv6']: tc_af = af @@ -298,23 +305,28 @@ class QoSBase: filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) + vlan_expression = "match.*.vif" + match_vlan = jmespath.search(vlan_expression, cls_config) + if any(tmp in ['exceed', 'bandwidth', 'burst'] for tmp in cls_config): - filter_cmd += f' action police' - - if 'exceed' in cls_config: - action = cls_config['exceed'] - filter_cmd += f' conform-exceed {action}' - if 'not_exceed' in cls_config: - action = cls_config['not_exceed'] - filter_cmd += f'/{action}' - - if 'bandwidth' in cls_config: - rate = self._rate_convert(cls_config['bandwidth']) - filter_cmd += f' rate {rate}' - - if 'burst' in cls_config: - burst = cls_config['burst'] - filter_cmd += f' burst {burst}' + # For "vif" "basic match" is used instead of "action police" T5961 + if not match_vlan: + filter_cmd += f' action police' + + if 'exceed' in cls_config: + action = cls_config['exceed'] + filter_cmd += f' conform-exceed {action}' + if 'not_exceed' in cls_config: + action = cls_config['not_exceed'] + filter_cmd += f'/{action}' + + if 'bandwidth' in cls_config: + rate = self._rate_convert(cls_config['bandwidth']) + filter_cmd += f' rate {rate}' + + if 'burst' in cls_config: + burst = cls_config['burst'] + filter_cmd += f' burst {burst}' cls = int(cls) filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) diff --git a/python/vyos/qos/trafficshaper.py b/python/vyos/qos/trafficshaper.py index 0d5f9a8a1..d6705cc77 100644 --- a/python/vyos/qos/trafficshaper.py +++ b/python/vyos/qos/trafficshaper.py @@ -1,4 +1,4 @@ -# Copyright 2022-2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2022-2024 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 @@ -99,7 +99,11 @@ class TrafficShaper(QoSBase): self._cmd(tmp) if 'default' in config: - rate = self._rate_convert(config['default']['bandwidth']) + if config['default']['bandwidth'].endswith('%'): + percent = config['default']['bandwidth'].rstrip('%') + rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + rate = self._rate_convert(config['default']['bandwidth']) burst = config['default']['burst'] quantum = config['default']['codel_quantum'] tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} htb rate {rate} burst {burst} quantum {quantum}' @@ -107,7 +111,11 @@ class TrafficShaper(QoSBase): priority = config['default']['priority'] tmp += f' prio {priority}' if 'ceiling' in config['default']: - f_ceil = self._rate_convert(config['default']['ceiling']) + if config['default']['ceiling'].endswith('%'): + percent = config['default']['ceiling'].rstrip('%') + f_ceil = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + f_ceil = self._rate_convert(config['default']['ceiling']) tmp += f' ceil {f_ceil}' self._cmd(tmp) @@ -117,8 +125,91 @@ class TrafficShaper(QoSBase): # call base class super().update(config, direction) -class TrafficShaperHFSC(TrafficShaper): +class TrafficShaperHFSC(QoSBase): + _parent = 1 + qostype = 'shaper_hfsc' + + # https://man7.org/linux/man-pages/man8/tc-hfsc.8.html def update(self, config, direction): + class_id_max = 0 + if 'class' in config: + tmp = list(config['class']) + tmp.sort() + class_id_max = tmp[-1] + + r2q = 10 + # bandwidth is a mandatory CLI node + speed = self._rate_convert(config['bandwidth']) + speed_bps = int(speed) // 8 + + # need a bigger r2q if going fast than 16 mbits/sec + if (speed_bps // r2q) >= MAXQUANTUM: # integer division + r2q = ceil(speed_bps // MAXQUANTUM) + else: + # if there is a slow class then may need smaller value + if 'class' in config: + min_speed = speed_bps + for cls, cls_options in config['class'].items(): + # find class with the lowest bandwidth used + if 'bandwidth' in cls_options: + bw_bps = int(self._rate_convert(cls_options['bandwidth'])) // 8 # bandwidth in bytes per second + if bw_bps < min_speed: + min_speed = bw_bps + + while (r2q > 1) and (min_speed // r2q) < MINQUANTUM: + tmp = r2q -1 + if (speed_bps // tmp) >= MAXQUANTUM: + break + r2q = tmp + + default_minor_id = int(class_id_max) +1 + tmp = f'tc qdisc replace dev {self._interface} root handle {self._parent:x}: hfsc default {default_minor_id:x}' # default is in hex + self._cmd(tmp) + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}: classid {self._parent:x}:1 hfsc sc rate {speed} ul rate {speed}' + self._cmd(tmp) + + if 'class' in config: + for cls, cls_config in config['class'].items(): + # class id is used later on and passed as hex, thus this needs to be an int + cls = int(cls) + # ls m1 + if cls_config.get('linkshare', {}).get('m1').endswith('%'): + percent = cls_config['linkshare']['m1'].rstrip('%') + m_one_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + m_one_rate = cls_config['linkshare']['m1'] + # ls m2 + if cls_config.get('linkshare', {}).get('m2').endswith('%'): + percent = cls_config['linkshare']['m2'].rstrip('%') + m_two_rate = self._rate_convert(config['bandwidth']) * int(percent) // 100 + else: + m_two_rate = self._rate_convert(cls_config['linkshare']['m2']) + + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{cls:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{cls:x} sfq perturb 10' + self._cmd(tmp) + + if 'default' in config: + # ls m1 + if config.get('default', {}).get('linkshare', {}).get('m1').endswith('%'): + percent = config['default']['linkshare']['m1'].rstrip('%') + m_one_rate = self._rate_convert(config['default']['linkshare']['m1']) * int(percent) // 100 + else: + m_one_rate = config['default']['linkshare']['m1'] + # ls m2 + if config.get('default', {}).get('linkshare', {}).get('m2').endswith('%'): + percent = config['default']['linkshare']['m2'].rstrip('%') + m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) * int(percent) // 100 + else: + m_two_rate = self._rate_convert(config['default']['linkshare']['m2']) + tmp = f'tc class replace dev {self._interface} parent {self._parent:x}:1 classid {self._parent:x}:{default_minor_id:x} hfsc ls m1 {m_one_rate} m2 {m_two_rate} ' + self._cmd(tmp) + + tmp = f'tc qdisc replace dev {self._interface} parent {self._parent:x}:{default_minor_id:x} sfq perturb 10' + self._cmd(tmp) + # call base class super().update(config, direction) - diff --git a/python/vyos/remote.py b/python/vyos/remote.py index b1efcd10b..830770d11 100644 --- a/python/vyos/remote.py +++ b/python/vyos/remote.py @@ -148,7 +148,7 @@ class FtpC: # Almost all FTP servers support the `SIZE' command. size = conn.size(self.path) if self.check_space: - check_storage(path, size) + check_storage(location, size) # No progressbar if we can't determine the size or if the file is too small. if self.progressbar and size and size > CHUNK_SIZE: with Progressbar(CHUNK_SIZE / size) as p: diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 319c3dabf..37b834ad6 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 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 @@ -27,7 +27,7 @@ TMPL_GRUB_COMPAT: str = 'grub/grub_compat.j2' # define regexes and variables REGEX_VERSION = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/[^}]*}' REGEX_MENUENTRY = r'^menuentry "[^\n]*{\n[^}]*\s+linux /boot/(?P<version>\S+)/vmlinuz (?P<options>[^\n]+)\n[^}]*}' -REGEX_CONSOLE = r'^.*console=(?P<console_type>[^\s\d]+)(?P<console_num>[\d]+).*$' +REGEX_CONSOLE = r'^.*console=(?P<console_type>[^\s\d]+)(?P<console_num>[\d]+)(,(?P<console_speed>[\d]+))?.*$' REGEX_SANIT_CONSOLE = r'\ ?console=[^\s\d]+[\d]+(,\d+)?\ ?' REGEX_SANIT_INIT = r'\ ?init=\S*\ ?' REGEX_SANIT_QUIET = r'\ ?quiet\ ?' @@ -131,6 +131,8 @@ def parse_entry(entry: tuple) -> dict: # find console type and number regex_filter = compile(REGEX_CONSOLE) entry_dict.update(regex_filter.match(entry[1]).groupdict()) + speed = entry_dict.get('console_speed', None) + entry_dict['console_speed'] = speed if speed is not None else '115200' entry_dict['boot_opts'] = sanitize_boot_opts(entry[1]) return entry_dict @@ -168,9 +170,12 @@ def prune_vyos_versions(root_dir: str = '') -> None: if not root_dir: root_dir = disk.find_persistence() - for version in grub.version_list(): + version_files = Path(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}').glob('*.cfg') + + for file in version_files: + version = Path(file).stem if not Path(f'{root_dir}/boot/{version}').is_dir(): - grub.version_del(version) + grub.version_del(version, root_dir) def update_cfg_ver(root_dir:str = '') -> int: @@ -244,13 +249,17 @@ def update_version_list(root_dir: str = '') -> list[dict]: menu_entries = list(filter(lambda x: x.get('version') != ver, menu_entries)) + # reset boot_opts in case of config update + for entry in menu_entries: + entry['boot_opts'] = grub.get_boot_opts(entry['version']) + add = list(set(current_versions) - set(menu_versions)) for ver in add: last = menu_entries[0].get('version') new = deepcopy(list(filter(lambda x: x.get('version') == last, menu_entries))) for e in new: - boot_opts = e.get('boot_opts').replace(last, ver) + boot_opts = grub.get_boot_opts(ver) e.update({'version': ver, 'boot_opts': boot_opts}) menu_entries = new + menu_entries @@ -271,9 +280,11 @@ def grub_cfg_fields(root_dir: str = '') -> dict: root_dir = disk.find_persistence() grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' + grub_vars = f'{root_dir}/{grub.CFG_VYOS_VARS}' - fields = {'default': 0, 'timeout': 5} - # 'default' and 'timeout' from legacy grub.cfg + fields = grub.vars_read(grub_vars) + # 'default' and 'timeout' from legacy grub.cfg resets 'default' to + # index, rather than uuid fields |= grub.vars_read(grub_cfg_main) fields['tools_version'] = SYSTEM_CFG_VER diff --git a/python/vyos/system/disk.py b/python/vyos/system/disk.py index b8a2c0f35..7860d719f 100644 --- a/python/vyos/system/disk.py +++ b/python/vyos/system/disk.py @@ -78,7 +78,7 @@ def parttable_create(drive_path: str, root_size: int) -> None: run(command) # update partitons in kernel sync() - run(f'partprobe {drive_path}') + run(f'partx -u {drive_path}') partitions: list[str] = partition_list(drive_path) diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index a94729964..2e8b20972 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 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 @@ -45,10 +45,14 @@ TMPL_GRUB_MODULES: str = 'grub/grub_modules.j2' TMPL_GRUB_OPTS: str = 'grub/grub_options.j2' TMPL_GRUB_COMMON: str = 'grub/grub_common.j2' +# default boot options +BOOT_OPTS_STEM: str = 'boot=live rootdelay=5 noautologin net.ifnames=0 biosdevname=0 vyos-union=/boot/' + # prepare regexes REGEX_GRUB_VARS: str = r'^set (?P<variable_name>.+)=[\'"]?(?P<variable_value>.*)(?<![\'"])[\'"]?$' REGEX_GRUB_MODULES: str = r'^insmod (?P<module_name>.+)$' REGEX_KERNEL_CMDLINE: str = r'^BOOT_IMAGE=/(?P<boot_type>boot|live)/((?P<image_version>.+)/)?vmlinuz.*$' +REGEX_GRUB_BOOT_OPTS: str = r'^\s*set boot_opts="(?P<boot_opts>[^$]+)"$' def install(drive_path: str, boot_dir: str, efi_dir: str, id: str = 'VyOS') -> None: @@ -95,7 +99,8 @@ def gen_version_uuid(version_name: str) -> str: def version_add(version_name: str, root_dir: str = '', - boot_opts: str = '') -> None: + boot_opts: str = '', + boot_opts_config = None) -> None: """Add a new VyOS version to GRUB loader configuration Args: @@ -112,7 +117,9 @@ def version_add(version_name: str, version_config, TMPL_VYOS_VERSION, { 'version_name': version_name, 'version_uuid': gen_version_uuid(version_name), - 'boot_opts': boot_opts + 'boot_opts_default': BOOT_OPTS_STEM + version_name, + 'boot_opts': boot_opts, + 'boot_opts_config': boot_opts_config }) @@ -294,12 +301,43 @@ def vars_write(grub_cfg: str, grub_vars: dict[str, str]) -> None: """ render(grub_cfg, TMPL_GRUB_VARS, {'vars': grub_vars}) +def get_boot_opts(version_name: str, root_dir: str = '') -> str: + """Read boot_opts setting from version file; return default setting on + any failure. + + Args: + version_name (str): version name + root_dir (str, optional): an optional path to the root directory. + Defaults to empty. + """ + if not root_dir: + root_dir = disk.find_persistence() + + boot_opts_default: str = BOOT_OPTS_STEM + version_name + boot_opts: str = '' + regex_filter = re_compile(REGEX_GRUB_BOOT_OPTS) + version_config: str = f'{root_dir}/{GRUB_DIR_VYOS_VERS}/{version_name}.cfg' + try: + config_text: list[str] = Path(version_config).read_text().splitlines() + except FileNotFoundError: + return boot_opts_default + for line in config_text: + search_result = regex_filter.fullmatch(line) + if search_result: + search_dict = search_result.groupdict() + boot_opts = search_dict.get('boot_opts', '') + break + + if not boot_opts: + boot_opts = boot_opts_default + + return boot_opts def set_default(version_name: str, root_dir: str = '') -> None: """Set version as default boot entry Args: - version_name (str): versio name + version_name (str): version name root_dir (str, optional): an optional path to the root directory. Defaults to empty. """ @@ -354,5 +392,33 @@ def set_console_type(console_type: str, root_dir: str = '') -> None: vars_current['console_type'] = str(console_type) vars_write(vars_file, vars_current) -def set_raid(root_dir: str = '') -> None: - pass +def set_console_speed(console_speed: str, root_dir: str = '') -> None: + """Write default console speed to GRUB configuration + + Args: + console_speed (str): default console speed + root_dir (str, optional): an optional path to the root directory. + Defaults to empty. + """ + if not root_dir: + root_dir = disk.find_persistence() + + vars_file: str = f'{root_dir}/{CFG_VYOS_VARS}' + vars_current: dict[str, str] = vars_read(vars_file) + vars_current['console_speed'] = str(console_speed) + vars_write(vars_file, vars_current) + +def set_kernel_cmdline_options(cmdline_options: str, version_name: str, + root_dir: str = '') -> None: + """Write additional cmdline options to GRUB configuration + + Args: + cmdline_options (str): cmdline options to add to default boot line + version_name (str): image version name + root_dir (str, optional): an optional path to the root directory. + """ + if not root_dir: + root_dir = disk.find_persistence() + + version_add(version_name=version_name, root_dir=root_dir, + boot_opts_config=cmdline_options) diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py new file mode 100644 index 000000000..4a3d8795e --- /dev/null +++ b/python/vyos/system/grub_util.py @@ -0,0 +1,70 @@ +# Copyright 2024 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/>. + +from vyos.system import disk, grub, image, compat + +@compat.grub_cfg_update +def set_console_speed(console_speed: str, root_dir: str = '') -> None: + """Write default console speed to GRUB configuration + + Args: + console_speed (str): default console speed + root_dir (str, optional): an optional path to the root directory. + Defaults to empty. + """ + if not root_dir: + root_dir = disk.find_persistence() + + grub.set_console_speed(console_speed, root_dir) + +@image.if_not_live_boot +def update_console_speed(console_speed: str, root_dir: str = '') -> None: + """Update console_speed if different from current value""" + + if not root_dir: + root_dir = disk.find_persistence() + + vars_file: str = f'{root_dir}/{grub.CFG_VYOS_VARS}' + vars_current: dict[str, str] = grub.vars_read(vars_file) + console_speed_current = vars_current.get('console_speed', None) + if console_speed != console_speed_current: + set_console_speed(console_speed, root_dir) + +@compat.grub_cfg_update +def set_kernel_cmdline_options(cmdline_options: str, version: str = '', + root_dir: str = '') -> None: + """Write Kernel CLI cmdline options to GRUB configuration""" + if not root_dir: + root_dir = disk.find_persistence() + + if not version: + version = image.get_running_image() + + grub.set_kernel_cmdline_options(cmdline_options, version, root_dir) + +@image.if_not_live_boot +def update_kernel_cmdline_options(cmdline_options: str, + root_dir: str = '') -> None: + """Update Kernel custom cmdline options""" + if not root_dir: + root_dir = disk.find_persistence() + + version = image.get_running_image() + + boot_opts_current = grub.get_boot_opts(version, root_dir) + boot_opts_proposed = grub.BOOT_OPTS_STEM + f'{version} {cmdline_options}' + + if boot_opts_proposed != boot_opts_current: + set_kernel_cmdline_options(cmdline_options, version, root_dir) diff --git a/python/vyos/system/image.py b/python/vyos/system/image.py index 514275654..5460e6a36 100644 --- a/python/vyos/system/image.py +++ b/python/vyos/system/image.py @@ -1,4 +1,4 @@ -# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2023-2024 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 @@ -15,6 +15,7 @@ from pathlib import Path from re import compile as re_compile +from functools import wraps from tempfile import TemporaryDirectory from typing import TypedDict @@ -262,6 +263,16 @@ def is_live_boot() -> bool: return True return False +def if_not_live_boot(func): + """Decorator to call function only if not live boot""" + @wraps(func) + def wrapper(*args, **kwargs): + if not is_live_boot(): + ret = func(*args, **kwargs) + return ret + return None + return wrapper + def is_running_as_container() -> bool: if Path('/.dockerenv').exists(): return True diff --git a/python/vyos/template.py b/python/vyos/template.py index 29ea0889b..456239568 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -786,6 +786,23 @@ def range_to_regex(num_range): regex = range_to_regex(num_range) return f'({regex})' +@register_filter('kea_address_json') +def kea_address_json(addresses): + from json import dumps + from vyos.utils.network import is_addr_assigned + + out = [] + + for address in addresses: + ifname = is_addr_assigned(address, return_ifname=True) + + if not ifname: + continue + + out.append(f'{ifname}/{address}') + + return dumps(out) + @register_filter('kea_failover_json') def kea_failover_json(config): from json import dumps @@ -842,15 +859,22 @@ def kea_shared_network_json(shared_networks): 'authoritative': ('authoritative' in config), 'subnet4': [] } - options = kea_parse_options(config) + + if 'option' in config: + network['option-data'] = kea_parse_options(config['option']) + + if 'bootfile_name' in config['option']: + network['boot-file-name'] = config['option']['bootfile_name'] + + if 'bootfile_server' in config['option']: + network['next-server'] = config['option']['bootfile_server'] if 'subnet' in config: for subnet, subnet_config in config['subnet'].items(): + if 'disable' in subnet_config: + continue network['subnet4'].append(kea_parse_subnet(subnet, subnet_config)) - if options: - network['option-data'] = options - out.append(network) return dumps(out, indent=4) @@ -870,7 +894,9 @@ def kea6_shared_network_json(shared_networks): 'name': name, 'subnet6': [] } - options = kea6_parse_options(config) + + if 'common_options' in config: + network['option-data'] = kea6_parse_options(config['common_options']) if 'interface' in config: network['interface'] = config['interface'] @@ -879,9 +905,6 @@ def kea6_shared_network_json(shared_networks): for subnet, subnet_config in config['subnet'].items(): network['subnet6'].append(kea6_parse_subnet(subnet, subnet_config)) - if options: - network['option-data'] = options - out.append(network) return dumps(out, indent=4) diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py index 997ee6309..cac59475d 100644 --- a/python/vyos/utils/network.py +++ b/python/vyos/utils/network.py @@ -159,7 +159,9 @@ def is_wwan_connected(interface): """ Determine if a given WWAN interface, e.g. wwan0 is connected to the carrier network or not """ import json + from vyos.utils.dict import dict_search from vyos.utils.process import cmd + from vyos.utils.process import is_systemd_service_active if not interface.startswith('wwan'): raise ValueError(f'Specified interface "{interface}" is not a WWAN interface') @@ -308,7 +310,7 @@ def is_ipv6_link_local(addr): return False -def is_addr_assigned(ip_address, vrf=None) -> bool: +def is_addr_assigned(ip_address, vrf=None, return_ifname=False) -> bool | str: """ Verify if the given IPv4/IPv6 address is assigned to any interface """ from netifaces import interfaces from vyos.utils.network import get_interface_config @@ -323,7 +325,7 @@ def is_addr_assigned(ip_address, vrf=None) -> bool: continue if is_intf_addr_assigned(interface, ip_address): - return True + return interface if return_ifname else True return False diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py index e09c7d86d..bd0644bc0 100644 --- a/python/vyos/utils/process.py +++ b/python/vyos/utils/process.py @@ -204,17 +204,32 @@ def process_running(pid_file): pid = f.read().strip() return pid_exists(int(pid)) -def process_named_running(name, cmdline: str=None): +def process_named_running(name: str, cmdline: str=None, timeout: int=0): """ Checks if process with given name is running and returns its PID. If Process is not running, return None """ from psutil import process_iter - for p in process_iter(['name', 'pid', 'cmdline']): - if cmdline: - if p.info['name'] == name and cmdline in p.info['cmdline']: + def check_process(name, cmdline): + for p in process_iter(['name', 'pid', 'cmdline']): + if cmdline: + if name in p.info['name'] and cmdline in p.info['cmdline']: + return p.info['pid'] + elif name in p.info['name']: return p.info['pid'] - elif p.info['name'] == name: - return p.info['pid'] + return None + if timeout: + import time + time_expire = time.time() + timeout + while True: + tmp = check_process(name, cmdline) + if not tmp: + if time.time() > time_expire: + break + time.sleep(0.100) # wait 250ms + continue + return tmp + else: + return check_process(name, cmdline) return None def is_systemd_service_active(service): diff --git a/smoketest/config-tests/basic-api-service b/smoketest/config-tests/basic-api-service index 1d2dc3472..dc54929b9 100644 --- a/smoketest/config-tests/basic-api-service +++ b/smoketest/config-tests/basic-api-service @@ -4,15 +4,11 @@ set interfaces loopback lo set service ntp server time1.vyos.net set service ntp server time2.vyos.net set service ntp server time3.vyos.net +set service https allow-client address '172.16.0.0/12' +set service https allow-client address '192.168.0.0/16' +set service https allow-client address '10.0.0.0/8' +set service https allow-client address '2001:db8::/32' set service https api keys id 1 key 'S3cur3' -set service https virtual-host bar allow-client address '172.16.0.0/12' -set service https virtual-host bar port '5555' -set service https virtual-host foo allow-client address '10.0.0.0/8' -set service https virtual-host foo allow-client address '2001:db8::/32' -set service https virtual-host foo port '7777' -set service https virtual-host baz allow-client address '192.168.0.0/16' -set service https virtual-host baz port '6666' -set service https virtual-host baz server-name 'baz' set system config-management commit-revisions '100' set system host-name 'vyos' set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/' diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos index ef8bf374a..0bb68b75d 100644 --- a/smoketest/config-tests/basic-vyos +++ b/smoketest/config-tests/basic-vyos @@ -24,12 +24,13 @@ set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50 set protocols static arp interface eth2.200.202 address 100.64.202.40 mac '00:50:00:00:00:40' set protocols static route 0.0.0.0/0 next-hop 100.64.0.1 set service dhcp-server shared-network-name LAN authoritative -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option name-server '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.20' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1' set service dns forwarding allow-from '192.168.0.0/16' set service dns forwarding cache-size '10000' set service dns forwarding dnssec 'off' diff --git a/smoketest/config-tests/bgp-medium-confederation b/smoketest/config-tests/bgp-medium-confederation new file mode 100644 index 000000000..ea3c2d144 --- /dev/null +++ b/smoketest/config-tests/bgp-medium-confederation @@ -0,0 +1,73 @@ +set interfaces dummy dum0 address '1.1.1.1/32' +set interfaces dummy dum0 address '2001:db8::1/128' +set interfaces ethernet eth0 address 'fd52:100:200:fffe::1/64' +set interfaces ethernet eth0 address '192.168.253.1/24' +set interfaces ethernet eth1 +set interfaces ethernet eth2 +set policy route-map BGP-IN rule 10 action 'permit' +set policy route-map BGP-OUT rule 10 action 'permit' +set policy route-map BGP-REDISTRIBUTE rule 10 action 'deny' +set policy route-map DEFAULT-ZEBRA-IN rule 10 action 'deny' +set protocols bgp address-family ipv4-unicast redistribute connected route-map 'BGP-REDISTRIBUTE' +set protocols bgp address-family ipv4-unicast redistribute static route-map 'BGP-REDISTRIBUTE' +set protocols bgp address-family ipv6-unicast redistribute connected route-map 'BGP-REDISTRIBUTE' +set protocols bgp neighbor 192.168.253.14 peer-group 'WDC07' +set protocols bgp neighbor 192.168.253.16 peer-group 'WDC07' +set protocols bgp neighbor 192.168.253.17 peer-group 'WDC07' +set protocols bgp neighbor 192.168.253.18 peer-group 'WDC07' +set protocols bgp neighbor 192.168.253.19 peer-group 'WDC07' +set protocols bgp neighbor eth1 interface v6only peer-group 'BACKBONE' +set protocols bgp neighbor eth1 interface v6only remote-as '666' +set protocols bgp neighbor eth2 interface v6only peer-group 'BACKBONE' +set protocols bgp neighbor eth2 interface v6only remote-as '666' +set protocols bgp neighbor fd52:100:200:fffe::14 address-family ipv6-unicast +set protocols bgp neighbor fd52:100:200:fffe::14 peer-group 'WDC07v6' +set protocols bgp neighbor fd52:100:200:fffe::16 address-family ipv6-unicast +set protocols bgp neighbor fd52:100:200:fffe::16 peer-group 'WDC07v6' +set protocols bgp neighbor fd52:100:200:fffe::17 address-family ipv6-unicast +set protocols bgp neighbor fd52:100:200:fffe::17 peer-group 'WDC07v6' +set protocols bgp neighbor fd52:100:200:fffe::18 address-family ipv6-unicast +set protocols bgp neighbor fd52:100:200:fffe::18 peer-group 'WDC07v6' +set protocols bgp neighbor fd52:100:200:fffe::19 address-family ipv6-unicast +set protocols bgp neighbor fd52:100:200:fffe::19 peer-group 'WDC07v6' +set protocols bgp parameters bestpath as-path confed +set protocols bgp parameters bestpath as-path multipath-relax +set protocols bgp parameters confederation identifier '696' +set protocols bgp parameters confederation peers '668' +set protocols bgp parameters confederation peers '669' +set protocols bgp parameters confederation peers '666' +set protocols bgp parameters graceful-restart +set protocols bgp parameters router-id '192.168.253.15' +set protocols bgp peer-group BACKBONE address-family ipv4-unicast nexthop-self +set protocols bgp peer-group BACKBONE address-family ipv4-unicast route-map export 'BGP-OUT' +set protocols bgp peer-group BACKBONE address-family ipv4-unicast route-map import 'BGP-IN' +set protocols bgp peer-group BACKBONE address-family ipv4-unicast soft-reconfiguration inbound +set protocols bgp peer-group BACKBONE address-family ipv6-unicast nexthop-self +set protocols bgp peer-group BACKBONE address-family ipv6-unicast route-map export 'BGP-OUT' +set protocols bgp peer-group BACKBONE address-family ipv6-unicast route-map import 'BGP-IN' +set protocols bgp peer-group BACKBONE address-family ipv6-unicast soft-reconfiguration inbound +set protocols bgp peer-group BACKBONE capability extended-nexthop +set protocols bgp peer-group WDC07 address-family ipv4-unicast default-originate +set protocols bgp peer-group WDC07 address-family ipv4-unicast nexthop-self +set protocols bgp peer-group WDC07 address-family ipv4-unicast route-map export 'BGP-OUT' +set protocols bgp peer-group WDC07 address-family ipv4-unicast route-map import 'BGP-IN' +set protocols bgp peer-group WDC07 address-family ipv4-unicast soft-reconfiguration inbound +set protocols bgp peer-group WDC07 remote-as '670' +set protocols bgp peer-group WDC07 update-source 'dum0' +set protocols bgp peer-group WDC07v6 address-family ipv6-unicast default-originate +set protocols bgp peer-group WDC07v6 address-family ipv6-unicast nexthop-self +set protocols bgp peer-group WDC07v6 address-family ipv6-unicast route-map export 'BGP-OUT' +set protocols bgp peer-group WDC07v6 address-family ipv6-unicast route-map import 'BGP-IN' +set protocols bgp peer-group WDC07v6 address-family ipv6-unicast soft-reconfiguration inbound +set protocols bgp peer-group WDC07v6 remote-as '670' +set protocols bgp peer-group WDC07v6 update-source 'dum0' +set protocols bgp system-as '670' +set system config-management commit-revisions '200' +set system console device ttyS0 speed '115200' +set system domain-name 'vyos.net' +set system host-name 'vyos' +set system ip protocol bgp route-map 'DEFAULT-ZEBRA-IN' +set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0' +set system login user vyos authentication plaintext-password '' +set system syslog global facility all level 'notice' +set system syslog global facility local7 level 'debug' diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index 8f3e4ade3..bdae6e807 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -254,11 +254,11 @@ set service dhcp-server failover remote '192.168.0.251' set service dhcp-server failover source-address '192.168.0.250' set service dhcp-server failover status 'primary' set service dhcp-server shared-network-name LAN authoritative -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 default-router '192.168.0.1' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-name 'vyos.net' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 domain-search 'vyos.net' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400' -set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 name-server '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-name 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option domain-search 'vyos.net' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option name-server '192.168.0.1' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic start '192.168.0.200' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 range LANDynamic stop '192.168.0.240' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping Audio ip-address '192.168.0.107' @@ -277,6 +277,7 @@ set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-map set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping pearTV mac '00:50:01:ba:62:79' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand ip-address '192.168.0.110' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping sand mac '00:50:01:af:c5:d2' +set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1' set service dns forwarding allow-from '192.168.0.0/16' set service dns forwarding cache-size '8192' set service dns forwarding dnssec 'off' diff --git a/smoketest/configs/basic-api-service b/smoketest/configs/basic-api-service index f5b56ac98..f997ccd73 100644 --- a/smoketest/configs/basic-api-service +++ b/smoketest/configs/basic-api-service @@ -29,6 +29,7 @@ service { allow-client { address 192.168.0.0/16 } + listen-address "*" listen-port 6666 server-name baz } diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos index fca4964bf..c42f14841 100644 --- a/smoketest/configs/basic-vyos +++ b/smoketest/configs/basic-vyos @@ -95,10 +95,15 @@ service { shared-network-name LAN6 { subnet fe88::/56 { address-range { - prefix fe88::/56 { + prefix fe88::/60 { temporary } + start fe88:0000:0000:fe:: { + stop fe88:0000:0000:ff:: + } } + domain-search vyos.net + name-server fe88::1 prefix-delegation { start fe88:0000:0000:0001:: { prefix-length 64 diff --git a/smoketest/configs/bgp-azure-ipsec-gateway b/smoketest/configs/bgp-azure-ipsec-gateway index ddcd459ae..5803e8ce9 100644 --- a/smoketest/configs/bgp-azure-ipsec-gateway +++ b/smoketest/configs/bgp-azure-ipsec-gateway @@ -226,6 +226,31 @@ protocols { } } service { + snmp { + v3 { + engineid 0xff42 + group default { + mode ro + seclevel priv + view default + } + user VyOS { + auth { + encrypted-key 0x1ad73f4620b8c0dd2de066622f875b161a14adad + type sha + } + group default + privacy { + encrypted-key 0x1ad73f4620b8c0dd2de066622f875b16 + type aes + } + } + view default { + oid 1 { + } + } + } + } ssh { disable-host-validation port 22 diff --git a/smoketest/configs/bgp-medium-confederation b/smoketest/configs/bgp-medium-confederation new file mode 100644 index 000000000..dfb944d09 --- /dev/null +++ b/smoketest/configs/bgp-medium-confederation @@ -0,0 +1,247 @@ +interfaces { + dummy dum0 { + address 1.1.1.1/32 + address 2001:db8::1/128 + } + ethernet eth0 { + address 192.168.253.1/24 + address fd52:100:200:fffe::1/64 + } + ethernet eth1 { + } + ethernet eth2 { + } +} +policy { + route-map BGP-IN { + rule 10 { + action permit + } + } + route-map BGP-OUT { + rule 10 { + action permit + } + } + route-map BGP-REDISTRIBUTE { + rule 10 { + action deny + } + } + route-map DEFAULT-ZEBRA-IN { + rule 10 { + action deny + } + } +} +protocols { + bgp 670 { + address-family { + ipv4-unicast { + redistribute { + connected { + route-map BGP-REDISTRIBUTE + } + static { + route-map BGP-REDISTRIBUTE + } + } + } + ipv6-unicast { + redistribute { + connected { + route-map BGP-REDISTRIBUTE + } + } + } + } + neighbor 192.168.253.14 { + peer-group WDC07 + } + neighbor 192.168.253.16 { + peer-group WDC07 + } + neighbor 192.168.253.17 { + peer-group WDC07 + } + neighbor 192.168.253.18 { + peer-group WDC07 + } + neighbor 192.168.253.19 { + peer-group WDC07 + } + neighbor eth1 { + interface { + v6only { + peer-group BACKBONE + remote-as 666 + } + } + } + neighbor eth2 { + interface { + v6only { + peer-group BACKBONE + remote-as 666 + } + } + } + neighbor fd52:100:200:fffe::14 { + address-family { + ipv6-unicast { + peer-group WDC07v6 + } + } + } + neighbor fd52:100:200:fffe::16 { + address-family { + ipv6-unicast { + peer-group WDC07v6 + } + } + } + neighbor fd52:100:200:fffe::17 { + address-family { + ipv6-unicast { + peer-group WDC07v6 + } + } + } + neighbor fd52:100:200:fffe::18 { + address-family { + ipv6-unicast { + peer-group WDC07v6 + } + } + } + neighbor fd52:100:200:fffe::19 { + address-family { + ipv6-unicast { + peer-group WDC07v6 + } + } + } + parameters { + bestpath { + as-path { + confed + multipath-relax + } + } + confederation { + identifier 696 + peers 668 + peers 669 + peers 666 + } + default { + no-ipv4-unicast + } + graceful-restart { + } + router-id 192.168.253.15 + } + peer-group BACKBONE { + address-family { + ipv4-unicast { + nexthop-self { + } + route-map { + export BGP-OUT + import BGP-IN + } + soft-reconfiguration { + inbound + } + } + ipv6-unicast { + nexthop-self { + } + route-map { + export BGP-OUT + import BGP-IN + } + soft-reconfiguration { + inbound + } + } + } + capability { + extended-nexthop + } + } + peer-group WDC07 { + address-family { + ipv4-unicast { + default-originate { + } + nexthop-self { + } + route-map { + export BGP-OUT + import BGP-IN + } + soft-reconfiguration { + inbound + } + } + } + remote-as 670 + update-source dum0 + } + peer-group WDC07v6 { + address-family { + ipv6-unicast { + default-originate { + } + nexthop-self { + } + route-map { + export BGP-OUT + import BGP-IN + } + soft-reconfiguration { + inbound + } + } + } + remote-as 670 + update-source dum0 + } + route-map DEFAULT-ZEBRA-IN + } +} +system { + config-management { + commit-revisions 200 + } + console { + device ttyS0 { + speed 115200 + } + } + domain-name vyos.net + host-name vyos + login { + user vyos { + authentication { + encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0 + plaintext-password "" + } + } + } + syslog { + global { + facility all { + level notice + } + facility protocols { + level debug + } + } + } +} + +// Warning: Do not remove the following line. +// vyos-config-version: "broadcast-relay@1:cluster@1:config-management@1:conntrack@3:conntrack-sync@2:container@1:dhcp-relay@2:dhcp-server@6:dhcpv6-server@1:dns-forwarding@3:firewall@5:https@2:interfaces@22:ipoe-server@1:ipsec@5:isis@1:l2tp@3:lldp@1:mdns@1:nat@5:ntp@1:pppoe-server@5:pptp@2:qos@1:quagga@8:rpki@1:salt@1:snmp@2:ssh@2:sstp@3:system@21:vrrp@2:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2:zone-policy@1" +// Release version: 1.3.5 diff --git a/smoketest/configs/ospf-small b/smoketest/configs/ospf-small index b3002b1af..c55627b77 100644 --- a/smoketest/configs/ospf-small +++ b/smoketest/configs/ospf-small @@ -1,13 +1,63 @@ interfaces { dummy dum0 { - address 172.18.254.201/32 + address 172.18.254.200/32 } ethernet eth0 { duplex auto smp-affinity auto speed auto vif 201 { - address 172.18.201.10/24 + address 172.18.201.9/24 + ip { + ospf { + authentication { + md5 { + key-id 10 { + md5-key OSPFVyOSNET + } + } + } + dead-interval 40 + hello-interval 10 + priority 1 + retransmit-interval 5 + transmit-delay 1 + } + } + ipv6 { + ospfv3 { + bfd + cost 40 + } + } + } + vif 202 { + address 172.18.202.9/24 + ip { + ospf { + authentication { + md5 { + key-id 10 { + md5-key OSPFVyOSNET + } + } + } + dead-interval 40 + hello-interval 10 + priority 1 + retransmit-interval 5 + transmit-delay 1 + } + } + ipv6 { + ospfv3 { + bfd + cost 40 + } + } + } + vif 203 { + address 172.18.203.9/24 ip { ospf { authentication { @@ -51,48 +101,31 @@ protocols { ospf { area 0 { network 172.18.201.0/24 - network 172.18.254.201/32 + network 172.18.202.0/24 + network 172.18.203.0/24 + network 172.18.254.200/32 } log-adjacency-changes { } parameters { abr-type cisco - router-id 172.18.254.201 + router-id 172.18.254.200 } passive-interface default passive-interface-exclude eth0.201 + passive-interface-exclude eth0.202 + passive-interface-exclude eth0.203 } ospfv3 { area 0.0.0.0 { - interface eth0 + interface eth0.201 + interface eth0.202 + interface eth0.203 interface eth1 - interface eth2 - } - } - static { - route 0.0.0.0/0 { - next-hop 172.18.201.254 { - distance 10 - } } } } service { - lldp { - interface all { - } - snmp { - enable - } - } - snmp { - community public { - authorization ro - network 172.16.100.0/24 - } - contact "VyOS maintainers and contributors <maintainers@vyos.io>" - location "Jenkins" - } ssh { disable-host-validation port 22 @@ -120,11 +153,9 @@ system { } name-server 172.16.254.30 ntp { - server 0.pool.ntp.org { - } - server 1.pool.ntp.org { + server time1.vyos.net { } - server 2.pool.ntp.org { + server time2.vyos.net { } } sysctl { diff --git a/smoketest/scripts/cli/base_accel_ppp_test.py b/smoketest/scripts/cli/base_accel_ppp_test.py index 1ea5db898..6219a0a4c 100644 --- a/smoketest/scripts/cli/base_accel_ppp_test.py +++ b/smoketest/scripts/cli/base_accel_ppp_test.py @@ -1,4 +1,4 @@ -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -441,3 +441,54 @@ class BasicAccelPPPTest: {second_subnet},name={second_pool},next={third_pool} {first_subnet},name={first_pool},next={second_pool}""" self.assertIn(pool_config, config) + + def test_accel_ipv6_pool(self): + # Test configuration of IPv6 client pools + self.basic_config(is_gateway=False, is_client_pool=False) + + # Enable IPv6 + allow_ipv6 = 'allow' + self.set(['ppp-options', 'ipv6', allow_ipv6]) + + pool_name = 'ipv6_test_pool' + prefix_1 = '2001:db8:fffe::/56' + prefix_mask = '64' + prefix_2 = '2001:db8:ffff::/56' + client_prefix_1 = f'{prefix_1},{prefix_mask}' + client_prefix_2 = f'{prefix_2},{prefix_mask}' + self.set( + ['client-ipv6-pool', pool_name, 'prefix', prefix_1, 'mask', + prefix_mask]) + self.set( + ['client-ipv6-pool', pool_name, 'prefix', prefix_2, 'mask', + prefix_mask]) + + delegate_1_prefix = '2001:db8:fff1::/56' + delegate_2_prefix = '2001:db8:fff2::/56' + delegate_mask = '64' + self.set( + ['client-ipv6-pool', pool_name, 'delegate', delegate_1_prefix, + 'delegation-prefix', delegate_mask]) + self.set( + ['client-ipv6-pool', pool_name, 'delegate', delegate_2_prefix, + 'delegation-prefix', delegate_mask]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', + strict=False) + conf.read(self._config_file) + + for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: + self.assertEqual(conf['modules'][tmp], None) + + self.assertEqual(conf['ppp']['ipv6'], allow_ipv6) + + config = self.getConfig("ipv6-pool") + pool_config = f"""{client_prefix_1},name={pool_name} +{client_prefix_2},name={pool_name} +delegate={delegate_1_prefix},{delegate_mask},name={pool_name} +delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" + self.assertIn(pool_config, config) diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py index 3f42196f7..a40b762a8 100644 --- a/smoketest/scripts/cli/base_interfaces_test.py +++ b/smoketest/scripts/cli/base_interfaces_test.py @@ -179,7 +179,7 @@ class BasicInterfaceTest: for interface in self._interfaces: # Check if dhclient process runs - dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface) + dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10) self.assertTrue(dhclient_pid) dhclient_config = read_file(f'{dhclient_base_dir}/dhclient_{interface}.conf') @@ -216,7 +216,7 @@ class BasicInterfaceTest: self.assertEqual(tmp, vrf_name) # Check if dhclient process runs - dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface) + dhclient_pid = process_named_running(dhclient_process_name, cmdline=interface, timeout=10) self.assertTrue(dhclient_pid) # .. inside the appropriate VRF instance vrf_pids = cmd(f'ip vrf pids {vrf_name}') @@ -251,7 +251,7 @@ class BasicInterfaceTest: self.assertEqual(tmp, vrf_name) # Check if dhclient process runs - tmp = process_named_running(dhcp6c_process_name, cmdline=interface) + tmp = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) self.assertTrue(tmp) # .. inside the appropriate VRF instance vrf_pids = cmd(f'ip vrf pids {vrf_name}') @@ -945,7 +945,7 @@ class BasicInterfaceTest: duid_base += 1 # Better ask the process about it's commandline in the future - pid = process_named_running(dhcp6c_process_name, cmdline=interface) + pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10) self.assertTrue(pid) dhcp6c_options = read_file(f'/proc/{pid}/cmdline') @@ -1004,7 +1004,7 @@ class BasicInterfaceTest: address = str(int(address) + 1) # Check for running process - self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface)) + self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)) for delegatee in delegatees: # we can already cleanup the test delegatee interface here @@ -1070,7 +1070,7 @@ class BasicInterfaceTest: address = str(int(address) + 1) # Check for running process - self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface)) + self.assertTrue(process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)) for delegatee in delegatees: # we can already cleanup the test delegatee interface here diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 66684b265..a7dd11145 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -752,8 +752,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ### Zone def test_zone_basic(self): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest', 'default-action', 'drop']) + self.cli_set(['firewall', 'ipv6', 'name', 'smoketestv6', 'default-action', 'drop']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'interface', 'eth0']) self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'from', 'smoketest-local', 'firewall', 'name', 'smoketest']) + self.cli_set(['firewall', 'zone', 'smoketest-eth0', 'intra-zone-filtering', 'firewall', 'ipv6-name', 'smoketestv6']) self.cli_set(['firewall', 'zone', 'smoketest-local', 'local-zone']) self.cli_set(['firewall', 'zone', 'smoketest-local', 'from', 'smoketest-eth0', 'firewall', 'name', 'smoketest']) self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept']) @@ -785,16 +787,30 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['ct state related', 'accept'] ] - nftables_output = cmd('sudo nft list table ip vyos_filter') - - for search in nftables_search: - matched = False - for line in nftables_output.split("\n"): - if all(item in line for item in search): - matched = True - break - self.assertTrue(matched) + nftables_search_v6 = [ + ['chain VYOS_ZONE_FORWARD'], + ['type filter hook forward priority filter + 1'], + ['chain VYOS_ZONE_OUTPUT'], + ['type filter hook output priority filter + 1'], + ['chain VYOS_ZONE_LOCAL'], + ['type filter hook input priority filter + 1'], + ['chain VZONE_smoketest-eth0'], + ['chain VZONE_smoketest-local_IN'], + ['chain VZONE_smoketest-local_OUT'], + ['oifname "eth0"', 'jump VZONE_smoketest-eth0'], + ['jump VZONE_smoketest-local_IN'], + ['jump VZONE_smoketest-local_OUT'], + ['iifname "eth0"', 'jump NAME6_smoketestv6'], + ['jump VYOS_STATE_POLICY6'], + ['chain VYOS_STATE_POLICY6'], + ['ct state established', 'log prefix "[STATE-POLICY-EST-A]"', 'accept'], + ['ct state invalid', 'drop'], + ['ct state related', 'accept'] + ] + nftables_output = cmd('sudo nft list table ip vyos_filter') + self.verify_nftables(nftables_search, 'ip vyos_filter') + self.verify_nftables(nftables_search_v6, 'ip6 vyos_filter') def test_flow_offload(self): self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0']) diff --git a/smoketest/scripts/cli/test_interfaces_ethernet.py b/smoketest/scripts/cli/test_interfaces_ethernet.py index a39b81348..e414f18cb 100755 --- a/smoketest/scripts/cli/test_interfaces_ethernet.py +++ b/smoketest/scripts/cli/test_interfaces_ethernet.py @@ -141,15 +141,18 @@ class EthernetInterfaceTest(BasicInterfaceTest.TestCase): # Verify that no address remains on the system as this is an eternal # interface. - for intf in self._interfaces: - self.assertNotIn(AF_INET, ifaddresses(intf)) + for interface in self._interfaces: + self.assertNotIn(AF_INET, ifaddresses(interface)) # required for IPv6 link-local address - self.assertIn(AF_INET6, ifaddresses(intf)) - for addr in ifaddresses(intf)[AF_INET6]: + self.assertIn(AF_INET6, ifaddresses(interface)) + for addr in ifaddresses(interface)[AF_INET6]: # checking link local addresses makes no sense if is_ipv6_link_local(addr['addr']): continue - self.assertFalse(is_intf_addr_assigned(intf, addr['addr'])) + self.assertFalse(is_intf_addr_assigned(interface, addr['addr'])) + # Ensure no VLAN interfaces are left behind + tmp = [x for x in Section.interfaces('ethernet') if x.startswith(f'{interface}.')] + self.assertListEqual(tmp, []) def test_offloading_rps(self): # enable RPS on all available CPUs, RPS works with a CPU bitmask, diff --git a/smoketest/scripts/cli/test_nat.py b/smoketest/scripts/cli/test_nat.py index 682fc141d..1e6435df8 100755 --- a/smoketest/scripts/cli/test_nat.py +++ b/smoketest/scripts/cli/test_nat.py @@ -292,5 +292,25 @@ class TestNAT(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_nat') + def test_snat_net_port_map(self): + self.cli_set(src_path + ['rule', '10', 'protocol', 'tcp_udp']) + self.cli_set(src_path + ['rule', '10', 'source', 'address', '100.64.0.0/25']) + self.cli_set(src_path + ['rule', '10', 'translation', 'address', '203.0.113.0/25']) + self.cli_set(src_path + ['rule', '10', 'translation', 'port', '1025-3072']) + + self.cli_set(src_path + ['rule', '20', 'protocol', 'tcp_udp']) + self.cli_set(src_path + ['rule', '20', 'source', 'address', '100.64.0.128/25']) + self.cli_set(src_path + ['rule', '20', 'translation', 'address', '203.0.113.128/25']) + self.cli_set(src_path + ['rule', '20', 'translation', 'port', '1025-3072']) + + self.cli_commit() + + nftables_search = [ + ['meta l4proto { tcp, udp }', 'snat ip prefix to ip saddr map { 100.64.0.0/25 : 203.0.113.0/25 . 1025-3072 }', 'comment "SRC-NAT-10"'], + ['meta l4proto { tcp, udp }', 'snat ip prefix to ip saddr map { 100.64.0.128/25 : 203.0.113.128/25 . 1025-3072 }', 'comment "SRC-NAT-20"'] + ] + + self.verify_nftables(nftables_search, 'ip vyos_nat') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_pki.py b/smoketest/scripts/cli/test_pki.py index 2ccc63b2c..02beafb26 100755 --- a/smoketest/scripts/cli/test_pki.py +++ b/smoketest/scripts/cli/test_pki.py @@ -19,6 +19,8 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM from vyos.configsession import ConfigSessionError +from vyos.utils.file import read_file + base_path = ['pki'] valid_ca_cert = """ @@ -153,10 +155,10 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestPKI, cls).setUpClass() - # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['service', 'https']) def tearDown(self): self.cli_delete(base_path) @@ -181,68 +183,72 @@ class TestPKI(VyOSUnitTestSHIM.TestCase): self.cli_commit() def test_invalid_ca_valid_certificate(self): - self.cli_set(base_path + ['ca', 'smoketest', 'certificate', valid_cert.replace('\n','')]) + self.cli_set(base_path + ['ca', 'invalid-ca', 'certificate', valid_cert.replace('\n','')]) with self.assertRaises(ConfigSessionError): self.cli_commit() def test_certificate_in_use(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'smoketest' + + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name]) self.cli_commit() - self.cli_delete(base_path + ['certificate', 'smoketest']) + self.cli_delete(base_path + ['certificate', cert_name]) with self.assertRaises(ConfigSessionError): self.cli_commit() self.cli_delete(['service', 'https', 'certificates', 'certificate']) def test_certificate_https_update(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'smoke-test_foo' + cert_path = f'/run/nginx/certs/{cert_name}_cert.pem' + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['service', 'https', 'certificates', 'certificate', 'smoketest']) + self.cli_set(['service', 'https', 'certificates', 'certificate', cert_name]) self.cli_commit() cert_data = None - with open('/etc/ssl/certs/smoketest.pem') as f: - cert_data = f.read() + cert_data = read_file(cert_path) - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')]) self.cli_commit() - with open('/etc/ssl/certs/smoketest.pem') as f: - self.assertNotEqual(cert_data, f.read()) + self.assertNotEqual(cert_data, read_file(cert_path)) self.cli_delete(['service', 'https', 'certificates', 'certificate']) def test_certificate_eapol_update(self): - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_ca_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_ca_private_key.replace('\n','')]) + cert_name = 'eapol' + interface = 'eth1' + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_ca_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_ca_private_key.replace('\n','')]) self.cli_commit() - self.cli_set(['interfaces', 'ethernet', 'eth1', 'eapol', 'certificate', 'smoketest']) + self.cli_set(['interfaces', 'ethernet', interface, 'eapol', 'certificate', cert_name]) self.cli_commit() cert_data = None - with open('/run/wpa_supplicant/eth1_cert.pem') as f: + with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f: cert_data = f.read() - self.cli_set(base_path + ['certificate', 'smoketest', 'certificate', valid_update_cert.replace('\n','')]) - self.cli_set(base_path + ['certificate', 'smoketest', 'private', 'key', valid_update_private_key.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'certificate', valid_update_cert.replace('\n','')]) + self.cli_set(base_path + ['certificate', cert_name, 'private', 'key', valid_update_private_key.replace('\n','')]) self.cli_commit() - with open('/run/wpa_supplicant/eth1_cert.pem') as f: + with open(f'/run/wpa_supplicant/{interface}_cert.pem') as f: self.assertNotEqual(cert_data, f.read()) - self.cli_delete(['interfaces', 'ethernet', 'eth1', 'eapol']) + self.cli_delete(['interfaces', 'ethernet', interface, 'eapol']) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_protocols_bfd.py b/smoketest/scripts/cli/test_protocols_bfd.py index f209eae3a..716d0a806 100755 --- a/smoketest/scripts/cli/test_protocols_bfd.py +++ b/smoketest/scripts/cli/test_protocols_bfd.py @@ -32,6 +32,7 @@ peers = { 'multihop' : '', 'source_addr': '192.0.2.254', 'profile' : 'foo-bar-baz', + 'minimum_ttl': '20', }, '192.0.2.20' : { 'echo_mode' : '', @@ -63,6 +64,7 @@ profiles = { 'intv_rx' : '222', 'intv_tx' : '333', 'shutdown' : '', + 'minimum_ttl': '40', }, 'foo-bar-baz' : { 'intv_mult' : '4', @@ -109,6 +111,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['peer', peer, 'interval', 'receive', peer_config["intv_rx"]]) if 'intv_tx' in peer_config: self.cli_set(base_path + ['peer', peer, 'interval', 'transmit', peer_config["intv_tx"]]) + if 'minimum_ttl' in peer_config: + self.cli_set(base_path + ['peer', peer, 'minimum-ttl', peer_config["minimum_ttl"]]) if 'multihop' in peer_config: self.cli_set(base_path + ['peer', peer, 'multihop']) if 'passive' in peer_config: @@ -152,6 +156,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertIn(f'receive-interval {peer_config["intv_rx"]}', peerconfig) if 'intv_tx' in peer_config: self.assertIn(f'transmit-interval {peer_config["intv_tx"]}', peerconfig) + if 'minimum_ttl' in peer_config: + self.assertIn(f'minimum-ttl {peer_config["minimum_ttl"]}', peerconfig) if 'passive' in peer_config: self.assertIn(f'passive-mode', peerconfig) if 'shutdown' in peer_config: @@ -173,6 +179,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['profile', profile, 'interval', 'receive', profile_config["intv_rx"]]) if 'intv_tx' in profile_config: self.cli_set(base_path + ['profile', profile, 'interval', 'transmit', profile_config["intv_tx"]]) + if 'minimum_ttl' in profile_config: + self.cli_set(base_path + ['profile', profile, 'minimum-ttl', profile_config["minimum_ttl"]]) if 'passive' in profile_config: self.cli_set(base_path + ['profile', profile, 'passive']) if 'shutdown' in profile_config: @@ -210,6 +218,8 @@ class TestProtocolsBFD(VyOSUnitTestSHIM.TestCase): self.assertIn(f' receive-interval {profile_config["intv_rx"]}', config) if 'intv_tx' in profile_config: self.assertIn(f' transmit-interval {profile_config["intv_tx"]}', config) + if 'minimum_ttl' in profile_config: + self.assertIn(f' minimum-ttl {profile_config["minimum_ttl"]}', config) if 'passive' in profile_config: self.assertIn(f' passive-mode', config) if 'shutdown' in profile_config: diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index ebc9eeaaa..2dbc30a41 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1139,10 +1139,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): def test_bgp_24_srv6_sid(self): locator_name = 'VyOS_foo' sid = 'auto' + nexthop_ipv4 = '192.0.0.1' + nexthop_ipv6 = '2001:db8:100:200::2' self.cli_set(base_path + ['srv6', 'locator', locator_name]) self.cli_set(base_path + ['sid', 'vpn', 'per-vrf', 'export', sid]) - + self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'sid', 'vpn', 'export', sid]) + # verify() - SID per VRF and SID per address-family are mutually exclusive! + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['address-family', 'ipv4-unicast', 'sid']) self.cli_commit() frrconfig = self.getFRRconfig(f'router bgp {ASN}') @@ -1151,7 +1157,64 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' locator {locator_name}', frrconfig) self.assertIn(f' sid vpn per-vrf export {sid}', frrconfig) - def test_bgp_25_bmp(self): + # Now test AFI SID + self.cli_delete(base_path + ['sid']) + self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'sid', 'vpn', 'export', sid]) + self.cli_set(base_path + ['address-family', 'ipv4-unicast', 'nexthop', 'vpn', 'export', nexthop_ipv4]) + self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'sid', 'vpn', 'export', sid]) + self.cli_set(base_path + ['address-family', 'ipv6-unicast', 'nexthop', 'vpn', 'export', nexthop_ipv6]) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' segment-routing srv6', frrconfig) + self.assertIn(f' locator {locator_name}', frrconfig) + + afiv4_config = self.getFRRconfig(' address-family ipv4 unicast') + self.assertIn(f' sid vpn export {sid}', afiv4_config) + self.assertIn(f' nexthop vpn export {nexthop_ipv4}', afiv4_config) + afiv6_config = self.getFRRconfig(' address-family ipv6 unicast') + self.assertIn(f' sid vpn export {sid}', afiv6_config) + self.assertIn(f' nexthop vpn export {nexthop_ipv6}', afiv4_config) + + def test_bgp_25_ipv4_ipv6_labeled_unicast_peer_group(self): + pg_ipv4 = 'foo4' + pg_ipv6 = 'foo6' + + ipv4_max_prefix = '20' + ipv6_max_prefix = '200' + ipv4_prefix = '192.0.2.0/24' + ipv6_prefix = '2001:db8:1000::/64' + + self.cli_set(base_path + ['listen', 'range', ipv4_prefix, 'peer-group', pg_ipv4]) + self.cli_set(base_path + ['listen', 'range', ipv6_prefix, 'peer-group', pg_ipv6]) + + self.cli_set(base_path + ['peer-group', pg_ipv4, 'address-family', 'ipv4-labeled-unicast', 'maximum-prefix', ipv4_max_prefix]) + self.cli_set(base_path + ['peer-group', pg_ipv4, 'remote-as', 'external']) + self.cli_set(base_path + ['peer-group', pg_ipv6, 'address-family', 'ipv6-labeled-unicast', 'maximum-prefix', ipv6_max_prefix]) + self.cli_set(base_path + ['peer-group', pg_ipv6, 'remote-as', 'external']) + + self.cli_commit() + + frrconfig = self.getFRRconfig(f'router bgp {ASN}') + self.assertIn(f'router bgp {ASN}', frrconfig) + self.assertIn(f' neighbor {pg_ipv4} peer-group', frrconfig) + self.assertIn(f' neighbor {pg_ipv4} remote-as external', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} peer-group', frrconfig) + self.assertIn(f' neighbor {pg_ipv6} remote-as external', frrconfig) + self.assertIn(f' bgp listen range {ipv4_prefix} peer-group {pg_ipv4}', frrconfig) + self.assertIn(f' bgp listen range {ipv6_prefix} peer-group {pg_ipv6}', frrconfig) + + afiv4_config = self.getFRRconfig(' address-family ipv4 labeled-unicast') + self.assertIn(f' neighbor {pg_ipv4} activate', afiv4_config) + self.assertIn(f' neighbor {pg_ipv4} maximum-prefix {ipv4_max_prefix}', afiv4_config) + + afiv6_config = self.getFRRconfig(' address-family ipv6 labeled-unicast') + self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) + self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) + + def test_bgp_99_bmp(self): target_name = 'instance-bmp' target_address = '127.0.0.1' target_port = '5000' diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index bf0c09965..194289567 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2021 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -32,6 +32,7 @@ CTRL_PROCESS_NAME = 'kea-ctrl-agent' KEA4_CONF = '/run/kea/kea-dhcp4.conf' KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket' base_path = ['service', 'dhcp-server'] +interface = 'dum8765' subnet = '192.0.2.0/25' router = inc_ip(subnet, 1) dns_1 = inc_ip(subnet, 2) @@ -46,11 +47,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) cidr_mask = subnet.split('/')[-1] - cls.cli_set(cls, ['interfaces', 'dummy', 'dum8765', 'address', f'{router}/{cidr_mask}']) + cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']) @classmethod def tearDownClass(cls): - cls.cli_delete(cls, ['interfaces', 'dummy', 'dum8765']) + cls.cli_delete(cls, ['interfaces', 'dummy', interface]) super(TestServiceDHCPServer, cls).tearDownClass() def tearDown(self): @@ -95,12 +96,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_start = inc_ip(subnet, 40) range_1_stop = inc_ip(subnet, 50) + self.cli_set(base_path + ['listen-interface', interface]) + pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -116,8 +120,10 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [interface]) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) @@ -164,30 +170,28 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): ipv6_only_preferred = '300' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) - self.cli_set(pool + ['ip-forwarding']) - self.cli_set(pool + ['smtp-server', smtp_server]) - self.cli_set(pool + ['pop-server', smtp_server]) - self.cli_set(pool + ['time-server', time_server]) - self.cli_set(pool + ['tftp-server-name', tftp_server]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) + self.cli_set(pool + ['option', 'ip-forwarding']) + self.cli_set(pool + ['option', 'smtp-server', smtp_server]) + self.cli_set(pool + ['option', 'pop-server', smtp_server]) + self.cli_set(pool + ['option', 'time-server', time_server]) + self.cli_set(pool + ['option', 'tftp-server-name', tftp_server]) for search in search_domains: - self.cli_set(pool + ['domain-search', search]) - self.cli_set(pool + ['bootfile-name', bootfile_name]) - self.cli_set(pool + ['bootfile-server', bootfile_server]) - self.cli_set(pool + ['wpad-url', wpad]) - self.cli_set(pool + ['server-identifier', server_identifier]) + self.cli_set(pool + ['option', 'domain-search', search]) + self.cli_set(pool + ['option', 'bootfile-name', bootfile_name]) + self.cli_set(pool + ['option', 'bootfile-server', bootfile_server]) + self.cli_set(pool + ['option', 'wpad-url', wpad]) + self.cli_set(pool + ['option', 'server-identifier', server_identifier]) - self.cli_set(pool + ['static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) - self.cli_set(pool + ['ipv6-only-preferred', ipv6_only_preferred]) - self.cli_set(pool + ['time-zone', 'Europe/London']) + self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1']) + self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred]) + self.cli_set(pool + ['option', 'time-zone', 'Europe/London']) - # check validate() - No DHCP address range or active static-mapping set - with self.assertRaises(ConfigSessionError): - self.cli_commit() self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -281,16 +285,85 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dhcp_single_pool_options_scoped(self): + shared_net_name = 'SMOKE-2' + + range_0_start = inc_ip(subnet, 10) + range_0_stop = inc_ip(subnet, 20) + + range_router = inc_ip(subnet, 5) + range_dns_1 = inc_ip(subnet, 6) + range_dns_2 = inc_ip(subnet, 7) + + shared_network = base_path + ['shared-network-name', shared_net_name] + pool = shared_network + ['subnet', subnet] + + self.cli_set(pool + ['subnet-id', '1']) + + # we use the first subnet IP address as default gateway + self.cli_set(shared_network + ['option', 'default-router', router]) + self.cli_set(shared_network + ['option', 'name-server', dns_1]) + self.cli_set(shared_network + ['option', 'name-server', dns_2]) + self.cli_set(shared_network + ['option', 'domain-name', domain_name]) + + self.cli_set(pool + ['range', '0', 'start', range_0_start]) + self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) + self.cli_set(pool + ['range', '0', 'option', 'default-router', range_router]) + self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_1]) + self.cli_set(pool + ['range', '0', 'option', 'name-server', range_dns_2]) + + # commit changes + self.cli_commit() + + config = read_file(KEA4_CONF) + obj = loads(config) + + self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) + + # Verify shared-network options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name', 'data': domain_name}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'option-data'], + {'name': 'routers', 'data': router}) + + # Verify range options + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'domain-name-servers', 'data': f'{range_dns_1}, {range_dns_2}'}) + self.verify_config_object( + obj, + ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools', 0, 'option-data'], + {'name': 'routers', 'data': range_router}) + + # Verify pool + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], 'pool', f'{range_0_start} - {range_0_stop}') + + # Check for running process + self.assertTrue(process_named_running(PROCESS_NAME)) + def test_dhcp_single_pool_static_mapping(self): shared_net_name = 'SMOKE-2' domain_name = 'private' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): @@ -309,6 +382,34 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.cli_delete(pool + ['static-mapping', 'client1', 'duid']) + # cannot have mappings with duplicate IP addresses + self.cli_set(pool + ['static-mapping', 'dupe1', 'mac', '00:50:00:00:fe:ff']) + self.cli_set(pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # Should allow disabled duplicate + self.cli_set(pool + ['static-mapping', 'dupe1', 'disable']) + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'dupe1']) + + # cannot have mappings with duplicate MAC addresses + self.cli_set(pool + ['static-mapping', 'dupe2', 'mac', '00:50:00:00:00:10']) + self.cli_set(pool + ['static-mapping', 'dupe2', 'ip-address', inc_ip(subnet, 120)]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'dupe2']) + + + # cannot have mappings with duplicate MAC addresses + self.cli_set(pool + ['static-mapping', 'dupe3', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) + self.cli_set(pool + ['static-mapping', 'dupe3', 'ip-address', inc_ip(subnet, 121)]) + self.cli_set(pool + ['static-mapping', 'dupe4', 'duid', '00:01:02:03:04:05:06:07:aa:aa:aa:aa:aa:01']) + self.cli_set(pool + ['static-mapping', 'dupe4', 'ip-address', inc_ip(subnet, 121)]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(pool + ['static-mapping', 'dupe3']) + self.cli_delete(pool + ['static-mapping', 'dupe4']) + # commit changes self.cli_commit() @@ -317,6 +418,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'id', 1) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'valid-lifetime', 86400) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'max-valid-lifetime', 86400) @@ -364,10 +466,11 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_1_stop = inc_ip(subnet, 40) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', str(int(network) + 1)]) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['domain-name', domain_name]) + self.cli_set(pool + ['option', 'default-router', router]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'domain-name', domain_name]) self.cli_set(pool + ['lease', lease_time]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) @@ -401,6 +504,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'id', int(network) + 1) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'valid-lifetime', int(lease_time)) self.verify_config_value(obj, ['Dhcp4', 'shared-networks', int(network), 'subnet4'], 'max-valid-lifetime', int(lease_time)) @@ -448,7 +552,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet] - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -490,7 +595,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_start_excl = inc_ip(exclude_addr, 1) pool = base_path + ['shared-network-name', 'EXCLUDE-TEST-2', 'subnet', subnet] - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['option', 'default-router', router]) self.cli_set(pool + ['exclude', exclude_addr]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -535,7 +641,8 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_stop = '10.0.250.255' pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet] - self.cli_set(pool + ['default-router', relay_router]) + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['option', 'default-router', relay_router]) self.cli_set(pool + ['range', '0', 'start', range_0_start]) self.cli_set(pool + ['range', '0', 'stop', range_0_stop]) @@ -545,6 +652,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): config = read_file(KEA4_CONF) obj = loads(config) + self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', [f'{interface}/{router}']) self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'RELAY') self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet) @@ -571,8 +679,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): range_0_stop = inc_ip(subnet, 20) pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] + self.cli_set(pool + ['subnet-id', '1']) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['default-router', router]) + self.cli_set(pool + ['option', 'default-router', router]) # check validate() - No DHCP address range or active static-mapping set with self.assertRaises(ConfigSessionError): diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py index f163cc69a..dcce30f55 100755 --- a/smoketest/scripts/cli/test_service_dhcpv6-server.py +++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py @@ -102,26 +102,27 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] self.cli_set(base_path + ['preference', preference]) - + self.cli_set(pool + ['subnet-id', '1']) # we use the first subnet IP address as default gateway - self.cli_set(pool + ['name-server', dns_1]) - self.cli_set(pool + ['name-server', dns_2]) - self.cli_set(pool + ['name-server', dns_2]) self.cli_set(pool + ['lease-time', 'default', lease_time]) self.cli_set(pool + ['lease-time', 'maximum', max_lease_time]) self.cli_set(pool + ['lease-time', 'minimum', min_lease_time]) - self.cli_set(pool + ['nis-domain', domain]) - self.cli_set(pool + ['nisplus-domain', domain]) - self.cli_set(pool + ['sip-server', sip_server]) - self.cli_set(pool + ['sntp-server', sntp_server]) - self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) + self.cli_set(pool + ['option', 'name-server', dns_1]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'name-server', dns_2]) + self.cli_set(pool + ['option', 'nis-domain', domain]) + self.cli_set(pool + ['option', 'nisplus-domain', domain]) + self.cli_set(pool + ['option', 'sip-server', sip_server]) + self.cli_set(pool + ['option', 'sntp-server', sntp_server]) + self.cli_set(pool + ['range', '1', 'start', range_start]) + self.cli_set(pool + ['range', '1', 'stop', range_stop]) for server in nis_servers: - self.cli_set(pool + ['nis-server', server]) - self.cli_set(pool + ['nisplus-server', server]) + self.cli_set(pool + ['option', 'nis-server', server]) + self.cli_set(pool + ['option', 'nisplus-server', server]) for search in search_domains: - self.cli_set(pool + ['domain-search', search]) + self.cli_set(pool + ['option', 'domain-search', search]) client_base = 1 for client in ['client1', 'client2', 'client3']: @@ -145,6 +146,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1) self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'valid-lifetime', int(lease_time)) self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'min-valid-lifetime', int(min_lease_time)) self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'max-valid-lifetime', int(max_lease_time)) @@ -215,8 +217,9 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): prefix_len = '56' pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet] - - self.cli_set(pool + ['address-range', 'start', range_start, 'stop', range_stop]) + self.cli_set(pool + ['subnet-id', '1']) + self.cli_set(pool + ['range', '1', 'start', range_start]) + self.cli_set(pool + ['range', '1', 'stop', range_stop]) self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'delegated-length', delegate_len]) self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'prefix-length', prefix_len]) @@ -250,7 +253,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_1]) self.cli_set(base_path + ['global-parameters', 'name-server', ns_global_2]) - self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]) + self.cli_set(base_path + ['shared-network-name', shared_net_name, 'subnet', subnet, 'subnet-id', '1']) # commit changes self.cli_commit() @@ -260,6 +263,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase): self.verify_config_value(obj, ['Dhcp6', 'shared-networks'], 'name', shared_net_name) self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'subnet', subnet) + self.verify_config_value(obj, ['Dhcp6', 'shared-networks', 0, 'subnet6'], 'id', 1) self.verify_config_object( obj, diff --git a/smoketest/scripts/cli/test_service_dns_dynamic.py b/smoketest/scripts/cli/test_service_dns_dynamic.py index ae46b18ba..c39d4467a 100755 --- a/smoketest/scripts/cli/test_service_dns_dynamic.py +++ b/smoketest/scripts/cli/test_service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -62,7 +62,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): 'zoneedit': {'protocol': 'zoneedit1', 'username': username}} for svc, details in services.items(): - self.cli_set(name_path + [svc, 'address', interface]) + self.cli_set(name_path + [svc, 'address', 'interface', interface]) self.cli_set(name_path + [svc, 'host-name', hostname]) self.cli_set(name_path + [svc, 'password', password]) for opt, value in details.items(): @@ -118,7 +118,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): expiry_time_bad = '360' self.cli_set(base_path + ['interval', interval]) - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['ip-version', ip_version]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) @@ -156,7 +156,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): ip_version = 'both' for name, details in services.items(): - self.cli_set(name_path + [name, 'address', interface]) + self.cli_set(name_path + [name, 'address', 'interface', interface]) self.cli_set(name_path + [name, 'host-name', hostname]) self.cli_set(name_path + [name, 'password', password]) for opt, value in details.items(): @@ -201,7 +201,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): with tempfile.NamedTemporaryFile(prefix='/config/auth/') as key_file: key_file.write(b'S3cretKey') - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['zone', zone]) @@ -229,7 +229,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): hostnames = ['@', 'www', hostname, f'@.{hostname}'] for name in hostnames: - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['username', username]) @@ -251,38 +251,38 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): # Check if DDNS service can be configured and runs svc_path = name_path + ['cloudflare'] proto = 'cloudflare' - web_url_good = 'https://ifconfig.me/ip' - web_url_bad = 'http:/ifconfig.me/ip' + web_url = 'https://ifconfig.me/ip' + web_skip = 'Current IP Address:' 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 + # not specifying either 'interface' or 'web' will raise an exception with self.assertRaises(ConfigSessionError): - self.cli_set(svc_path + ['address', interface]) self.cli_commit() self.cli_set(svc_path + ['address', 'web']) - # commit changes + # specifying both 'interface' and 'web' will raise an exception as well + with self.assertRaises(ConfigSessionError): + self.cli_set(svc_path + ['address', 'interface', interface]) + self.cli_commit() + self.cli_delete(svc_path + ['address', 'interface']) self.cli_commit() - # web-options must be a valid URL + # web option 'skip' is useless without the option 'url' with self.assertRaises(ConfigSessionError): - self.cli_set(svc_path + ['web-options', 'url', web_url_bad]) + self.cli_set(svc_path + ['address', 'web', 'skip', web_skip]) self.cli_commit() - self.cli_set(svc_path + ['web-options', 'url', web_url_good]) - - # commit changes + self.cli_set(svc_path + ['address', 'web', 'url', web_url]) self.cli_commit() # Check the generating config parameters ddclient_conf = cmd(f'sudo cat {DDCLIENT_CONF}') self.assertIn(f'usev4=webv4', ddclient_conf) - self.assertIn(f'webv4={web_url_good}', ddclient_conf) + self.assertIn(f'webv4={web_url}', ddclient_conf) + self.assertIn(f'webv4-skip=\'{web_skip}\'', ddclient_conf) self.assertIn(f'protocol={proto}', ddclient_conf) self.assertIn(f'zone={zone}', ddclient_conf) self.assertIn(f'password=\'{password}\'', ddclient_conf) @@ -294,7 +294,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): proto = 'namecheap' dyn_interface = 'pppoe587' - self.cli_set(svc_path + ['address', dyn_interface]) + self.cli_set(svc_path + ['address', 'interface', dyn_interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['server', server]) self.cli_set(svc_path + ['username', username]) @@ -327,7 +327,7 @@ class TestServiceDDNS(VyOSUnitTestSHIM.TestCase): self.cli_set(['vrf', 'name', vrf_name, 'table', vrf_table]) self.cli_set(base_path + ['vrf', vrf_name]) - self.cli_set(svc_path + ['address', interface]) + self.cli_set(svc_path + ['address', 'interface', interface]) self.cli_set(svc_path + ['protocol', proto]) self.cli_set(svc_path + ['host-name', hostname]) self.cli_set(svc_path + ['zone', zone]) diff --git a/smoketest/scripts/cli/test_service_dns_forwarding.py b/smoketest/scripts/cli/test_service_dns_forwarding.py index bc50a4ffe..079c584ba 100755 --- a/smoketest/scripts/cli/test_service_dns_forwarding.py +++ b/smoketest/scripts/cli/test_service_dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2022 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -24,9 +24,10 @@ from vyos.template import bracketize_ipv6 from vyos.utils.file import read_file from vyos.utils.process import process_named_running -CONFIG_FILE = '/run/powerdns/recursor.conf' -FORWARD_FILE = '/run/powerdns/recursor.forward-zones.conf' -HOSTSD_FILE = '/run/powerdns/recursor.vyos-hostsd.conf.lua' +PDNS_REC_RUN_DIR = '/run/pdns-recursor' +CONFIG_FILE = f'{PDNS_REC_RUN_DIR}/recursor.conf' +FORWARD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' +HOSTSD_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' PROCESS_NAME= 'pdns_recursor' base_path = ['service', 'dns', 'forwarding'] @@ -43,7 +44,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): super(TestServicePowerDNS, cls).setUpClass() - # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) @@ -59,11 +59,23 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertFalse(process_named_running(PROCESS_NAME)) + def setUp(self): + # forward to base class + super().setUp() + for network in allow_from: + self.cli_set(base_path + ['allow-from', network]) + for address in listen_adress: + self.cli_set(base_path + ['listen-address', address]) + def test_basic_forwarding(self): # Check basic DNS forwarding settings cache_size = '20' negative_ttl = '120' + # remove code from setUp() as in this test-case we validate the proper + # handling of assertions when specific CLI nodes are missing + self.cli_delete(base_path) + self.cli_set(base_path + ['cache-size', cache_size]) self.cli_set(base_path + ['negative-ttl', negative_ttl]) @@ -118,12 +130,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): def test_dnssec(self): # DNSSEC option testing - - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - options = ['off', 'process-no-validate', 'process', 'log-fail', 'validate'] for option in options: self.cli_set(base_path + ['dnssec', option]) @@ -136,12 +142,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): def test_external_nameserver(self): # Externe Domain Name Servers (DNS) addresses - - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - nameservers = {'192.0.2.1': {}, '192.0.2.2': {'port': '53'}, '2001:db8::1': {'port': '853'}} for h,p in nameservers.items(): if 'port' in p: @@ -163,11 +163,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): self.assertEqual(tmp, 'yes') def test_domain_forwarding(self): - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - domains = ['vyos.io', 'vyos.net', 'vyos.com'] nameservers = {'192.0.2.1': {}, '192.0.2.2': {'port': '53'}, '2001:db8::1': {'port': '853'}} for domain in domains: @@ -204,11 +199,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): self.assertIn(f'addNTA("{domain}", "static")', hosts_conf) def test_no_rfc1918_forwarding(self): - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - self.cli_set(base_path + ['no-serve-rfc1918']) # commit changes @@ -220,12 +210,6 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): def test_dns64(self): dns_prefix = '64:ff9b::/96' - - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - # Check dns64-prefix - must be prefix /96 self.cli_set(base_path + ['dns64-prefix', '2001:db8:aabb::/64']) with self.assertRaises(ConfigSessionError): @@ -239,21 +223,73 @@ class TestServicePowerDNS(VyOSUnitTestSHIM.TestCase): tmp = get_config_value('dns64-prefix') self.assertEqual(tmp, dns_prefix) + def test_exclude_throttle_adress(self): + exclude_throttle_adress_examples = [ + '192.168.128.255', + '10.0.0.0/25', + '2001:db8:85a3:8d3:1319:8a2e:370:7348', + '64:ff9b::/96' + ] + for exclude_throttle_adress in exclude_throttle_adress_examples: + self.cli_set(base_path + ['exclude-throttle-address', exclude_throttle_adress]) + + # commit changes + self.cli_commit() + + # verify dont-throttle-netmasks configuration + tmp = get_config_value('dont-throttle-netmasks') + self.assertEqual(tmp, ','.join(exclude_throttle_adress_examples)) + + def test_serve_stale_extension(self): + server_stale = '20' + self.cli_set(base_path + ['serve-stale-extension', server_stale]) + # commit changes + self.cli_commit() + # verify configuration + tmp = get_config_value('serve-stale-extensions') + self.assertEqual(tmp, server_stale) + def test_listening_port(self): # We can listen on a different port compared to '53' but only one at a time - for port in ['1053', '5353']: + for port in ['10053', '10054']: self.cli_set(base_path + ['port', port]) - for network in allow_from: - self.cli_set(base_path + ['allow-from', network]) - for address in listen_adress: - self.cli_set(base_path + ['listen-address', address]) - # commit changes self.cli_commit() - # verify local-port configuration tmp = get_config_value('local-port') self.assertEqual(tmp, port) + def test_ecs_add_for(self): + options = ['0.0.0.0/0', '!10.0.0.0/8', 'fc00::/7', '!fe80::/10'] + for param in options: + self.cli_set(base_path + ['options', 'ecs-add-for', param]) + + # commit changes + self.cli_commit() + # verify ecs_add_for configuration + tmp = get_config_value('ecs-add-for') + self.assertEqual(tmp, ','.join(options)) + + def test_ecs_ipv4_bits(self): + option_value = '24' + self.cli_set(base_path + ['options', 'ecs-ipv4-bits', option_value]) + # commit changes + self.cli_commit() + # verify ecs_ipv4_bits configuration + tmp = get_config_value('ecs-ipv4-bits') + self.assertEqual(tmp, option_value) + + def test_edns_subnet_allow_list(self): + options = ['192.0.2.1/32', 'example.com', 'fe80::/10'] + for param in options: + self.cli_set(base_path + ['options', 'edns-subnet-allow-list', param]) + + # commit changes + self.cli_commit() + + # verify edns_subnet_allow_list configuration + tmp = get_config_value('edns-subnet-allow-list') + self.assertEqual(tmp, ','.join(options)) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py index 280932fd7..8d9b8459e 100755 --- a/smoketest/scripts/cli/test_service_https.py +++ b/smoketest/scripts/cli/test_service_https.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -23,6 +23,7 @@ from urllib3.exceptions import InsecureRequestWarning from base_vyostest_shim import VyOSUnitTestSHIM from base_vyostest_shim import ignore_warning from vyos.utils.file import read_file +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import process_named_running @@ -52,7 +53,22 @@ MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgPLpD0Ohhoq0g4nhx u8/3jHMM7sDwL3aWzW/zp54/LhCWUoLMjDdDEEigK4fal4ZF9aA9F0Ww """ +dh_1024 = """ +MIGHAoGBAM3nvMkHGi/xmRs8cYg4pcl5sAanxel9EM+1XobVhUViXw8JvlmSEVOj +n2aXUifc4SEs3WDzVPRC8O8qQWjvErpTq/HOgt3aqBCabMgvflmt706XP0KiqnpW +EyvNiI27J3wBUzEXLIS110MxPAX5Tcug974PecFcOxn1RWrbWcx/AgEC +""" + +dh_2048 = """ +MIIBCAKCAQEA1mld/V7WnxxRinkOlhx/BoZkRELtIUQFYxyARBqYk4C5G3YnZNNu +zjaGyPnfIKHu8SIUH85OecM+5/co9nYlcUJuph2tbR6qNgPw7LOKIhf27u7WhvJk +iVsJhwZiWmvvMV4jTParNEI2svoooMyhHXzeweYsg6YtgLVmwiwKj3XP3gRH2i3B +Mq8CDS7X6xaKvjfeMPZBFqOM5nb6HhsbaAUyiZxrfipLvXxtnbzd/eJUQVfVdxM3 +pn0i+QrO2tuNAzX7GoPc9pefrbb5xJmGS50G0uqsR59+7LhYmyZSBASA0lxTEW9t +kv/0LPvaYTY57WL7hBeqqHy/WPZHPzDI3wIBAg== +""" # to test load config via HTTP URL +nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest' nginx_conf_smoketest = """ server { listen 8000; @@ -81,6 +97,11 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): cls.cli_delete(cls, base_path) cls.cli_delete(cls, pki_base) + @classmethod + def tearDownClass(cls): + super(TestHTTPSService, cls).tearDownClass() + call(f'sudo rm -f {nginx_tmp_site}') + def tearDown(self): self.cli_delete(base_path) self.cli_delete(pki_base) @@ -89,33 +110,31 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): # Check for stopped process self.assertFalse(process_named_running(PROCESS_NAME)) - def test_server_block(self): - vhost_id = 'example' - address = '0.0.0.0' - port = '8443' - name = 'example.org' - - test_path = base_path + ['virtual-host', vhost_id] - - self.cli_set(test_path + ['listen-address', address]) - self.cli_set(test_path + ['port', port]) - self.cli_set(test_path + ['server-name', name]) - - self.cli_commit() - - nginx_config = read_file('/etc/nginx/sites-enabled/default') - self.assertIn(f'listen {address}:{port} ssl;', nginx_config) - self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) - self.assertTrue(process_named_running(PROCESS_NAME)) - def test_certificate(self): - self.cli_set(pki_base + ['certificate', 'test_https', 'certificate', cert_data.replace('\n','')]) - self.cli_set(pki_base + ['certificate', 'test_https', 'private', 'key', key_data.replace('\n','')]) - - self.cli_set(base_path + ['certificates', 'certificate', 'test_https']) + cert_name = 'test_https' + dh_name = 'dh-test' + + self.cli_set(base_path + ['certificates', 'certificate', cert_name]) + # verify() - certificates do not exist (yet) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(pki_base + ['certificate', cert_name, 'certificate', cert_data.replace('\n','')]) + self.cli_set(pki_base + ['certificate', cert_name, 'private', 'key', key_data.replace('\n','')]) + + self.cli_set(base_path + ['certificates', 'dh-params', dh_name]) + # verify() - dh-params do not exist (yet) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + + self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_1024.replace('\n','')]) + # verify() - dh-param minimum length is 2048 bit + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(pki_base + ['dh', dh_name, 'parameters', dh_2048.replace('\n','')]) self.cli_commit() self.assertTrue(process_named_running(PROCESS_NAME)) + self.debug = False def test_api_missing_keys(self): self.cli_set(base_path + ['api']) @@ -135,15 +154,13 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): key = 'MySuperSecretVyOS' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) - test_path = base_path + ['virtual-host', vhost_id] - self.cli_set(test_path + ['listen-address', address]) - self.cli_set(test_path + ['server-name', name]) + self.cli_set(base_path + ['listen-address', address]) self.cli_commit() nginx_config = read_file('/etc/nginx/sites-enabled/default') self.assertIn(f'listen {address}:{port} ssl;', nginx_config) - self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) + self.assertIn(f'ssl_protocols TLSv1.2 TLSv1.3;', nginx_config) # default url = f'https://{address}/retrieve' payload = {'data': '{"op": "showConfig", "path": []}', 'key': f'{key}'} @@ -402,19 +419,15 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): url_config = f'https://{address}/configure' headers = {} tmp_file = 'tmp-config.boot' - nginx_tmp_site = '/etc/nginx/sites-enabled/smoketest' self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key]) self.cli_commit() # load config via HTTP requires nginx config call(f'sudo touch {nginx_tmp_site}') - call(f'sudo chown vyos:vyattacfg {nginx_tmp_site}') - call(f'sudo chmod +w {nginx_tmp_site}') - - with open(nginx_tmp_site, 'w') as f: - f.write(nginx_conf_smoketest) - call('sudo nginx -s reload') + call(f'sudo chmod 666 {nginx_tmp_site}') + write_file(nginx_tmp_site, nginx_conf_smoketest) + call('sudo systemctl reload nginx') # save config payload = { @@ -441,8 +454,8 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase): self.assertEqual(r.status_code, 200) # cleanup tmp nginx conf - call(f'sudo rm -rf {nginx_tmp_site}') - call('sudo nginx -s reload') + call(f'sudo rm -f {nginx_tmp_site}') + call('sudo systemctl reload nginx') if __name__ == '__main__': unittest.main(verbosity=5) diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index 6e95b3bd1..cec6adb09 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-2023 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -188,6 +188,45 @@ gw-ip-address={third_gateway.split('/')[0]} {first_subnet},name={first_pool},next={second_pool}""" self.assertIn(pool_config, config) + def test_accel_ipv6_pool(self): + # Test configuration of IPv6 client pools + self.basic_config(is_gateway=False, is_client_pool=False) + + pool_name = 'ipv6_test_pool' + prefix_1 = '2001:db8:fffe::/56' + prefix_mask = '64' + prefix_2 = '2001:db8:ffff::/56' + client_prefix_1 = f'{prefix_1},{prefix_mask}' + client_prefix_2 = f'{prefix_2},{prefix_mask}' + self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_1, 'mask', + prefix_mask]) + self.set(['client-ipv6-pool', pool_name, 'prefix', prefix_2, 'mask', + prefix_mask]) + + delegate_1_prefix = '2001:db8:fff1::/56' + delegate_2_prefix = '2001:db8:fff2::/56' + delegate_mask = '64' + self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_1_prefix, + 'delegation-prefix', delegate_mask]) + self.set(['client-ipv6-pool', pool_name, 'delegate', delegate_2_prefix, + 'delegation-prefix', delegate_mask]) + + # commit changes + self.cli_commit() + + # Validate configuration values + conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False) + conf.read(self._config_file) + + for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: + self.assertEqual(conf['modules'][tmp], None) + + config = self.getConfig("ipv6-pool") + pool_config = f"""{client_prefix_1},name={pool_name} +{client_prefix_2},name={pool_name} +delegate={delegate_1_prefix},{delegate_mask},name={pool_name} +delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" + self.assertIn(pool_config, config) if __name__ == "__main__": unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ntp.py b/smoketest/scripts/cli/test_service_ntp.py index 5e385d5ad..ae45fe2f4 100755 --- a/smoketest/scripts/cli/test_service_ntp.py +++ b/smoketest/scripts/cli/test_service_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -43,7 +43,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.assertFalse(process_named_running(PROCESS_NAME)) - def test_01_ntp_options(self): + def test_base_options(self): # Test basic NTP support with multiple servers and their options servers = ['192.0.2.1', '192.0.2.2'] options = ['nts', 'noselect', 'prefer'] @@ -77,7 +77,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for pool in pools: self.assertIn(f'pool {pool} iburst', config) - def test_02_ntp_clients(self): + def test_clients(self): # Test the allowed-networks statement listen_address = ['127.0.0.1', '::1'] for listen in listen_address: @@ -107,7 +107,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for listen in listen_address: self.assertIn(f'bindaddress {listen}', config) - def test_03_ntp_interface(self): + def test_interface(self): interfaces = ['eth0'] for interface in interfaces: self.cli_set(base_path + ['interface', interface]) @@ -124,7 +124,7 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): for interface in interfaces: self.assertIn(f'binddevice {interface}', config) - def test_04_ntp_vrf(self): + def test_vrf(self): vrf_name = 'vyos-mgmt' self.cli_set(['vrf', 'name', vrf_name, 'table', '12345']) @@ -142,5 +142,28 @@ class TestSystemNTP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['vrf', 'name', vrf_name]) + def test_leap_seconds(self): + servers = ['time1.vyos.net', 'time2.vyos.net'] + for server in servers: + self.cli_set(base_path + ['server', server]) + + self.cli_commit() + + # Check generated client address configuration + # this file must be read with higher permissions + config = cmd(f'sudo cat {NTP_CONF}') + self.assertIn('leapsectz right/UTC', config) # CLI default + + for mode in ['ignore', 'system', 'smear']: + self.cli_set(base_path + ['leap-second', mode]) + self.cli_commit() + config = cmd(f'sudo cat {NTP_CONF}') + if mode != 'smear': + self.assertIn(f'leapsecmode {mode}', config) + else: + self.assertIn(f'leapsecmode slew', config) + self.assertIn(f'maxslewrate 1000', config) + self.assertIn(f'smoothtime 400 0.001024 leaponly', config) + 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 fa3bb87db..11d5b8b78 100755 --- a/smoketest/scripts/cli/test_service_pppoe-server.py +++ b/smoketest/scripts/cli/test_service_pppoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2023 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -93,6 +93,13 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): interface_cache = '128000' self.set(['ppp-options', 'interface-cache', interface_cache]) + # ipv6 + allow_ipv6 = 'allow' + random = 'random' + self.set(['ppp-options', 'ipv6', allow_ipv6]) + self.set(['ppp-options', 'ipv6-intf-id', random]) + self.set(['ppp-options', 'ipv6-accept-peer-intf-id']) + self.set(['ppp-options', 'ipv6-peer-intf-id', random]) # commit changes self.cli_commit() @@ -118,6 +125,15 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): # check interface-cache self.assertEqual(conf['ppp']['unit-cache'], interface_cache) + #check ipv6 + for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: + self.assertEqual(conf['modules'][tmp], None) + + self.assertEqual(conf['ppp']['ipv6'], allow_ipv6) + self.assertEqual(conf['ppp']['ipv6-intf-id'], random) + self.assertEqual(conf['ppp']['ipv6-peer-intf-id'], random) + self.assertTrue(conf['ppp'].getboolean('ipv6-accept-peer-intf-id')) + def test_pppoe_server_authentication_protocols(self): # Test configuration of local authentication for PPPoE server self.basic_config() @@ -154,45 +170,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase): 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 - self.basic_config() - - # Enable IPv6 - allow_ipv6 = 'allow' - random = 'random' - self.set(['ppp-options', 'ipv6', allow_ipv6]) - self.set(['ppp-options', 'ipv6-intf-id', random]) - self.set(['ppp-options', 'ipv6-accept-peer-intf-id']) - self.set(['ppp-options', 'ipv6-peer-intf-id', random]) - - prefix = '2001:db8:ffff::/64' - prefix_mask = '128' - client_prefix = f'{prefix},{prefix_mask}' - self.set(['client-ipv6-pool', 'prefix', prefix, 'mask', prefix_mask]) - - delegate_prefix = '2001:db8::/40' - delegate_mask = '56' - self.set(['client-ipv6-pool', 'delegate', delegate_prefix, 'delegation-prefix', delegate_mask]) - - # commit changes - self.cli_commit() - - # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') - conf.read(self._config_file) - - for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: - self.assertEqual(conf['modules'][tmp], None) - - self.assertEqual(conf['ppp']['ipv6'], allow_ipv6) - self.assertEqual(conf['ppp']['ipv6-intf-id'], random) - self.assertEqual(conf['ppp']['ipv6-peer-intf-id'], random) - self.assertTrue(conf['ppp'].getboolean('ipv6-accept-peer-intf-id')) - - 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' diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index 0dbc97d49..cea34138e 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -31,6 +31,14 @@ def get_sysctl(parameter): return read_file(f'/proc/sys/{tmp}') class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): + @classmethod + def setUpClass(cls): + super(TestSystemConntrack, cls).setUpClass() + + # ensure we can also run this test on a live system - so lets clean + # out the current configuration :) + cls.cli_delete(cls, base_path) + def tearDown(self): self.cli_delete(base_path) self.cli_commit() diff --git a/smoketest/scripts/cli/test_system_sflow.py b/smoketest/scripts/cli/test_system_sflow.py index 63262db69..c0424d915 100755 --- a/smoketest/scripts/cli/test_system_sflow.py +++ b/smoketest/scripts/cli/test_system_sflow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,6 +17,7 @@ import unittest from base_vyostest_shim import VyOSUnitTestSHIM +from time import sleep from vyos.configsession import ConfigSessionError from vyos.ifconfig import Section @@ -26,12 +27,11 @@ from vyos.utils.file import read_file PROCESS_NAME = 'hsflowd' base_path = ['system', 'sflow'] +vrf = 'mgmt' hsflowd_conf = '/run/sflow/hsflowd.conf' - class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): - @classmethod def setUpClass(cls): super(TestSystemFlowAccounting, cls).setUpClass() @@ -45,6 +45,7 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): self.assertTrue(process_named_running(PROCESS_NAME)) self.cli_delete(base_path) + self.cli_delete(['vrf', 'name', vrf]) self.cli_commit() # after service removal process must no longer run @@ -96,6 +97,27 @@ class TestSystemFlowAccounting(VyOSUnitTestSHIM.TestCase): for interface in Section.interfaces('ethernet'): self.assertIn(f'pcap {{ dev={interface} }}', hsflowd) + def test_vrf(self): + interface = 'eth0' + server = '192.0.2.1' + + # Check if sFlow service can be bound to given VRF + self.cli_set(['vrf', 'name', vrf, 'table', '10100']) + self.cli_set(base_path + ['interface', interface]) + self.cli_set(base_path + ['server', server]) + self.cli_set(base_path + ['vrf', vrf]) + + # commit changes + self.cli_commit() + + # verify configuration + hsflowd = read_file(hsflowd_conf) + self.assertIn(f'collector {{ ip = {server} udpport = 6343 }}', hsflowd) # default port + self.assertIn(f'pcap {{ dev=eth0 }}', hsflowd) + + # Check for process in VRF + tmp = cmd(f'ip vrf pids {vrf}') + self.assertIn(PROCESS_NAME, tmp) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_vpn_ipsec.py b/smoketest/scripts/cli/test_vpn_ipsec.py index 17e12bcaf..f5369ee7a 100755 --- a/smoketest/scripts/cli/test_vpn_ipsec.py +++ b/smoketest/scripts/cli/test_vpn_ipsec.py @@ -115,6 +115,7 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase): # ensure we can also run this test on a live system - so lets clean # out the current configuration :) cls.cli_delete(cls, base_path) + cls.cli_delete(cls, ['pki']) cls.cli_set(cls, base_path + ['interface', f'{interface}.{vif}']) diff --git a/smoketest/scripts/cli/test_vpn_l2tp.py b/smoketest/scripts/cli/test_vpn_l2tp.py index 5b3e419bd..129a9c602 100755 --- a/smoketest/scripts/cli/test_vpn_l2tp.py +++ b/smoketest/scripts/cli/test_vpn_l2tp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -55,6 +55,13 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.set(['ppp-options', 'lcp-echo-interval', lcp_echo_interval]) self.set(['ppp-options', 'lcp-echo-timeout', lcp_echo_timeout]) + allow_ipv6 = 'allow' + random = 'random' + self.set(['ppp-options', 'ipv6', allow_ipv6]) + self.set(['ppp-options', 'ipv6-intf-id', random]) + self.set(['ppp-options', 'ipv6-accept-peer-intf-id']) + self.set(['ppp-options', 'ipv6-peer-intf-id', random]) + # commit changes self.cli_commit() @@ -76,6 +83,13 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['ppp']['lcp-echo-timeout'], lcp_echo_timeout) self.assertEqual(conf['ppp']['lcp-echo-failure'], lcp_echo_failure) + for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: + self.assertEqual(conf['modules'][tmp], None) + self.assertEqual(conf['ppp']['ipv6'], allow_ipv6) + self.assertEqual(conf['ppp']['ipv6-intf-id'], random) + self.assertEqual(conf['ppp']['ipv6-peer-intf-id'], random) + self.assertTrue(conf['ppp'].getboolean('ipv6-accept-peer-intf-id')) + def test_l2tp_server_authentication_protocols(self): # Test configuration of local authentication for PPPoE server self.basic_config() @@ -92,44 +106,6 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase): self.assertEqual(conf['modules']['auth_mschap_v2'], None) - def test_l2tp_server_client_ipv6_pool(self): - # Test configuration of IPv6 client pools - self.basic_config() - - # Enable IPv6 - allow_ipv6 = 'allow' - random = 'random' - self.set(['ppp-options', 'ipv6', allow_ipv6]) - self.set(['ppp-options', 'ipv6-intf-id', random]) - self.set(['ppp-options', 'ipv6-accept-peer-intf-id']) - self.set(['ppp-options', 'ipv6-peer-intf-id', random]) - - prefix = '2001:db8:ffff::/64' - prefix_mask = '128' - client_prefix = f'{prefix},{prefix_mask}' - self.set(['client-ipv6-pool', 'prefix', prefix, 'mask', prefix_mask]) - - delegate_prefix = '2001:db8::/40' - delegate_mask = '56' - self.set(['client-ipv6-pool', 'delegate', delegate_prefix, 'delegation-prefix', delegate_mask]) - - # commit changes - self.cli_commit() - - # Validate configuration values - conf = ConfigParser(allow_no_value=True, delimiters='=') - conf.read(self._config_file) - - for tmp in ['ipv6pool', 'ipv6_nd', 'ipv6_dhcp']: - self.assertEqual(conf['modules'][tmp], None) - - self.assertEqual(conf['ppp']['ipv6'], allow_ipv6) - self.assertEqual(conf['ppp']['ipv6-intf-id'], random) - self.assertEqual(conf['ppp']['ipv6-peer-intf-id'], random) - self.assertTrue(conf['ppp'].getboolean('ipv6-accept-peer-intf-id')) - - self.assertEqual(conf['ipv6-pool'][client_prefix], None) - self.assertEqual(conf['ipv6-pool']['delegate'], f'{delegate_prefix},{delegate_mask}') 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 index 0d9ea312e..f3fce822b 100755 --- a/smoketest/scripts/cli/test_vpn_pptp.py +++ b/smoketest/scripts/cli/test_vpn_pptp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -218,6 +218,10 @@ class TestVPNPPTPServer(BasicAccelPPPTest.TestCase): self.assertEqual(f"req-limit=0", server[4]) self.assertEqual(f"fail-time=0", server[5]) + @unittest.skip("IPv6 is not implemented in PPTP") + def test_accel_ipv6_pool(self): + pass + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/system/test_module_load.py b/smoketest/scripts/system/test_module_load.py index 9d94f01e6..fc60c7220 100755 --- a/smoketest/scripts/system/test_module_load.py +++ b/smoketest/scripts/system/test_module_load.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,7 +18,7 @@ import unittest from vyos.utils.process import cmd modules = { - "intel": ["e1000", "e1000e", "igb", "ixgb", "ixgbe", "ixgbevf", "i40e", + "intel": ["e1000", "e1000e", "igb", "ixgbe", "ixgbevf", "i40e", "i40evf", "iavf"], "intel_qat": ["qat_200xx", "qat_200xxvf", "qat_c3xxx", "qat_c3xxxvf", "qat_c62x", "qat_c62xvf", "qat_d15xx", "qat_d15xxvf", diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py index 19b206c59..26822b755 100755 --- a/src/conf_mode/nat.py +++ b/src/conf_mode/nat.py @@ -87,11 +87,6 @@ def verify_rule(config, err_msg, groups_dict): raise ConfigError(f'{err_msg} ports can only be specified when '\ 'protocol is either tcp, udp or tcp_udp!') - if is_ip_network(dict_search('translation.address', config)): - raise ConfigError(f'{err_msg} cannot use ports with an IPv4 network as '\ - 'translation address as it statically maps a whole network '\ - 'of addresses onto another network of addresses!') - for side in ['destination', 'source']: if side in config: side_conf = config[side] diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py index f7e14aa16..4be40e99e 100755 --- a/src/conf_mode/pki.py +++ b/src/conf_mode/pki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,59 +14,66 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os + +from sys import argv from sys import exit from vyos.config import Config -from vyos.configdep import set_dependents, call_dependents +from vyos.config import config_dict_merge +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configdict import node_changed +from vyos.configdiff import Diff +from vyos.defaults import directories from vyos.pki import is_ca_certificate from vyos.pki import load_certificate from vyos.pki import load_public_key from vyos.pki import load_private_key from vyos.pki import load_crl from vyos.pki import load_dh_parameters +from vyos.utils.boot import boot_configuration_complete +from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.dict import dict_search_recursive +from vyos.utils.process import call +from vyos.utils.process import cmd +from vyos.utils.process import is_systemd_service_active from vyos import ConfigError from vyos import airbag airbag.enable() -# keys to recursively search for under specified path, script to call if update required +vyos_certbot_dir = directories['certbot'] + +# keys to recursively search for under specified path sync_search = [ { 'keys': ['certificate'], 'path': ['service', 'https'], - 'script': '/usr/libexec/vyos/conf_mode/service_https.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['interfaces', 'ethernet'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_ethernet.py' }, { 'keys': ['certificate', 'ca_certificate', 'dh_params', 'shared_secret_key', 'auth_key', 'crypt_key'], 'path': ['interfaces', 'openvpn'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_openvpn.py' }, { 'keys': ['ca_certificate'], 'path': ['interfaces', 'sstpc'], - 'script': '/usr/libexec/vyos/conf_mode/interfaces_sstpc.py' }, { 'keys': ['certificate', 'ca_certificate', 'local_key', 'remote_key'], 'path': ['vpn', 'ipsec'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_ipsec.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'openconnect'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_openconnect.py' }, { 'keys': ['certificate', 'ca_certificate'], 'path': ['vpn', 'sstp'], - 'script': '/usr/libexec/vyos/conf_mode/vpn_sstp.py' } ] @@ -82,6 +89,33 @@ sync_translate = { 'crypt_key': 'openvpn' } +def certbot_delete(certificate): + if not boot_configuration_complete(): + return + if os.path.exists(f'{vyos_certbot_dir}/renewal/{certificate}.conf'): + cmd(f'certbot delete --non-interactive --config-dir {vyos_certbot_dir} --cert-name {certificate}') + +def certbot_request(name: str, config: dict, dry_run: bool=True): + # We do not call certbot when booting the system - there is no need to do so and + # request new certificates during boot/image upgrade as the certbot configuration + # is stored persistent under /config - thus we do not open the door to transient + # errors + if not boot_configuration_complete(): + return + + domains = '--domains ' + ' --domains '.join(config['domain_name']) + tmp = f'certbot certonly --non-interactive --config-dir {vyos_certbot_dir} --cert-name {name} '\ + f'--standalone --agree-tos --no-eff-email --expand --server {config["url"]} '\ + f'--email {config["email"]} --key-type rsa --rsa-key-size {config["rsa_key_size"]} '\ + f'{domains}' + if 'listen_address' in config: + tmp += f' --http-01-address {config["listen_address"]}' + # verify() does not need to actually request a cert but only test for plausability + if dry_run: + tmp += ' --dry-run' + + cmd(tmp, raising=ConfigError, message=f'ACME certbot request failed for "{name}"!') + def get_config(config=None): if config: conf = config @@ -93,25 +127,60 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) - pki['changed'] = {} - tmp = node_changed(conf, base + ['ca'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'ca' : tmp}) + if len(argv) > 1 and argv[1] == 'certbot_renew': + pki['certbot_renew'] = {} - tmp = node_changed(conf, base + ['certificate'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'certificate' : tmp}) + tmp = node_changed(conf, base + ['ca'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'ca' : tmp}) - tmp = node_changed(conf, base + ['dh'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'dh' : tmp}) + tmp = node_changed(conf, base + ['certificate'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'certificate' : tmp}) - tmp = node_changed(conf, base + ['key-pair'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'key_pair' : tmp}) + tmp = node_changed(conf, base + ['dh'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'dh' : tmp}) - tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], key_mangling=('-', '_'), recursive=True) - if tmp: pki['changed'].update({'openvpn' : tmp}) + tmp = node_changed(conf, base + ['key-pair'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'key_pair' : tmp}) + + tmp = node_changed(conf, base + ['openvpn', 'shared-secret'], recursive=True) + if tmp: + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'openvpn' : tmp}) # We only merge on the defaults of there is a configuration at all if conf.exists(base): - pki = conf.merge_defaults(pki, recursive=True) + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**pki.kwargs, recursive=True) + # remove ACME default configuration if unused by CLI + if 'certificate' in pki: + for name, cert_config in pki['certificate'].items(): + if 'acme' not in cert_config: + # Remove ACME default values + del default_values['certificate'][name]['acme'] + + # merge CLI and default dictionary + pki = config_dict_merge(default_values, pki) + + # Certbot triggered an external renew of the certificates. + # Mark all ACME based certificates as "changed" to trigger + # update of dependent services + if 'certificate' in pki and 'certbot_renew' in pki: + renew = [] + for name, cert_config in pki['certificate'].items(): + if 'acme' in cert_config: + renew.append(name) + # If triggered externally by certbot, certificate key is not present in changed + if 'changed' not in pki: pki.update({'changed':{}}) + pki['changed'].update({'certificate' : renew}) # We need to get the entire system configuration to verify that we are not # deleting a certificate that is still referenced somewhere! @@ -119,38 +188,34 @@ def get_config(config=None): get_first_key=True, no_tag_node_value_mangle=True) - if 'changed' in pki: - for search in sync_search: - for key in search['keys']: - changed_key = sync_translate[key] - - if changed_key not in pki['changed']: - continue - - for item_name in pki['changed'][changed_key]: - node_present = False - if changed_key == 'openvpn': - node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) - else: - node_present = dict_search_args(pki, changed_key, item_name) - - if node_present: - search_dict = dict_search_args(pki['system'], *search['path']) - - if not search_dict: - continue - - for found_name, found_path in dict_search_recursive(search_dict, key): - if found_name == item_name: - path = search['path'] - path_str = ' '.join(path + found_path) - print(f'pki: Updating config: {path_str} {found_name}') - - if path[0] == 'interfaces': - ifname = found_path[0] - set_dependents(path[1], conf, ifname) - else: - set_dependents(path[1], conf) + for search in sync_search: + for key in search['keys']: + changed_key = sync_translate[key] + if 'changed' not in pki or changed_key not in pki['changed']: + continue + + for item_name in pki['changed'][changed_key]: + node_present = False + if changed_key == 'openvpn': + node_present = dict_search_args(pki, 'openvpn', 'shared_secret', item_name) + else: + node_present = dict_search_args(pki, changed_key, item_name) + + if node_present: + search_dict = dict_search_args(pki['system'], *search['path']) + if not search_dict: + continue + for found_name, found_path in dict_search_recursive(search_dict, key): + if found_name == item_name: + path = search['path'] + path_str = ' '.join(path + found_path) + print(f'PKI: Updating config: {path_str} {found_name}') + + if path[0] == 'interfaces': + ifname = found_path[0] + set_dependents(path[1], conf, ifname) + else: + set_dependents(path[1], conf) return pki @@ -223,6 +288,22 @@ def verify(pki): if not is_valid_private_key(private['key'], protected): raise ConfigError(f'Invalid private key on certificate "{name}"') + if 'acme' in cert_conf: + if 'domain_name' not in cert_conf['acme']: + raise ConfigError(f'At least one domain-name is required to request '\ + f'certificate for "{name}" via ACME!') + + if 'email' not in cert_conf['acme']: + raise ConfigError(f'An email address is required to request '\ + f'certificate for "{name}" via ACME!') + + if 'certbot_renew' not in pki: + # Only run the ACME command if something on this entity changed, + # as this is time intensive + tmp = dict_search('changed.certificate', pki) + if tmp != None and name in tmp: + certbot_request(name, cert_conf['acme']) + if 'dh' in pki: for name, dh_conf in pki['dh'].items(): if 'parameters' in dh_conf: @@ -283,12 +364,58 @@ def generate(pki): if not pki: return None + # Certbot renewal only needs to re-trigger the services to load up the + # new PEM file + if 'certbot_renew' in pki: + return None + + certbot_list = [] + certbot_list_on_disk = [] + if os.path.exists(f'{vyos_certbot_dir}/live'): + certbot_list_on_disk = [f.path.split('/')[-1] for f in os.scandir(f'{vyos_certbot_dir}/live') if f.is_dir()] + + if 'certificate' in pki: + changed_certificates = dict_search('changed.certificate', pki) + for name, cert_conf in pki['certificate'].items(): + if 'acme' in cert_conf: + certbot_list.append(name) + # generate certificate if not found on disk + if name not in certbot_list_on_disk: + certbot_request(name, cert_conf['acme'], dry_run=False) + elif changed_certificates != None and name in changed_certificates: + # when something for the certificate changed, we should delete it + if name in certbot_list_on_disk: + certbot_delete(name) + certbot_request(name, cert_conf['acme'], dry_run=False) + + # Cleanup certbot configuration and certificates if no longer in use by CLI + # Get foldernames under vyos_certbot_dir which each represent a certbot cert + if os.path.exists(f'{vyos_certbot_dir}/live'): + for cert in certbot_list_on_disk: + if cert not in certbot_list: + # certificate is no longer active on the CLI - remove it + certbot_delete(cert) + return None def apply(pki): + systemd_certbot_name = 'certbot.timer' if not pki: + call(f'systemctl stop {systemd_certbot_name}') return None + has_certbot = False + if 'certificate' in pki: + for name, cert_conf in pki['certificate'].items(): + if 'acme' in cert_conf: + has_certbot = True + break + + if not has_certbot: + call(f'systemctl stop {systemd_certbot_name}') + elif has_certbot and not is_systemd_service_active(systemd_certbot_name): + call(f'systemctl restart {systemd_certbot_name}') + if 'changed' in pki: call_dependents() diff --git a/src/conf_mode/protocols_bfd.py b/src/conf_mode/protocols_bfd.py index dab784662..37421efb4 100755 --- a/src/conf_mode/protocols_bfd.py +++ b/src/conf_mode/protocols_bfd.py @@ -72,6 +72,9 @@ def verify(bfd): if 'source' in peer_config and 'interface' in peer_config['source']: raise ConfigError('BFD multihop and source interface cannot be used together') + if 'minimum_ttl' in peer_config and 'multihop' not in peer_config: + raise ConfigError('Minimum TTL is only available for multihop BFD sessions!') + if 'profile' in peer_config: profile_name = peer_config['profile'] if 'profile' not in bfd or profile_name not in bfd['profile']: diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index bf807fa5f..d90dfe45b 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -206,6 +206,10 @@ def verify_remote_as(peer_config, bgp_config): if 'v6only' in peer_config['interface']: if 'remote_as' in peer_config['interface']['v6only']: return peer_config['interface']['v6only']['remote_as'] + if 'peer_group' in peer_config['interface']['v6only']: + peer_group_name = peer_config['interface']['v6only']['peer_group'] + tmp = dict_search(f'peer_group.{peer_group_name}.remote_as', bgp_config) + if tmp: return tmp return None @@ -216,9 +220,12 @@ def verify_afi(peer_config, bgp_config): # If address_family configured under peer-group # if neighbor interface configured - peer_group_name = '' + peer_group_name = None if dict_search('interface.peer_group', peer_config): peer_group_name = peer_config['interface']['peer_group'] + elif dict_search('interface.v6only.peer_group', peer_config): + peer_group_name = peer_config['interface']['v6only']['peer_group'] + # if neighbor IP configured. if 'peer_group' in peer_config: peer_group_name = peer_config['peer_group'] @@ -502,6 +509,14 @@ def verify(bgp): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): raise ConfigError( 'Command "import vrf" conflicts with "route-target vpn both" command!') + if dict_search('route_target.vpn.export', afi_config): + raise ConfigError( + 'Command "route-target vpn export" conflicts '\ + 'with "route-target vpn both" command!') + if dict_search('route_target.vpn.import', afi_config): + raise ConfigError( + 'Command "route-target vpn import" conflicts '\ + 'with "route-target vpn both" command!') if dict_search('route_target.vpn.import', afi_config): if verify_vrf_as_import(vrf_name, afi, bgp['dependent_vrfs']): @@ -538,6 +553,10 @@ def verify(bgp): tmp = dict_search(f'route_map.vpn.{export_import}', afi_config) if tmp: verify_route_map(tmp, bgp) + # per-vrf sid and per-af sid are mutually exclusive + if 'sid' in afi_config and 'sid' in bgp: + raise ConfigError('SID per VRF and SID per address-family are mutually exclusive!') + # Checks only required for L2VPN EVPN if afi in ['l2vpn_evpn']: if 'vni' in afi_config: diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index ce67ccff7..8d594bb68 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -220,7 +220,20 @@ def verify(isis): if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') - + + # Check for index ranges being larger than the segment routing global block + if dict_search('segment_routing.global_block', isis): + g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) + g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) + g_label_difference = int(g_high_label_value) - int(g_low_label_value) + if dict_search('segment_routing.prefix', isis): + for prefix, prefix_config in isis['segment_routing']['prefix'].items(): + if 'index' in prefix_config: + index_size = isis['segment_routing']['prefix'][prefix]['index']['value'] + if int(index_size) > int(g_label_difference): + raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ + f'index base size larger than the SRGB label base.') + # Check for LFA tiebreaker index duplication if dict_search('fast_reroute.lfa.local.tiebreaker', isis): comparison_dictionary = {} diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 2f07142a3..34cf49286 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -213,6 +213,19 @@ def verify(ospf): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') + # Check for index ranges being larger than the segment routing global block + if dict_search('segment_routing.global_block', ospf): + g_high_label_value = dict_search('segment_routing.global_block.high_label_value', ospf) + g_low_label_value = dict_search('segment_routing.global_block.low_label_value', ospf) + g_label_difference = int(g_high_label_value) - int(g_low_label_value) + if dict_search('segment_routing.prefix', ospf): + for prefix, prefix_config in ospf['segment_routing']['prefix'].items(): + if 'index' in prefix_config: + index_size = ospf['segment_routing']['prefix'][prefix]['index']['value'] + if int(index_size) > int(g_label_difference): + raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ + f'index base size larger than the SRGB label base.') + # Check route summarisation if 'summary_address' in ospf: for prefix, prefix_options in ospf['summary_address'].items(): diff --git a/src/conf_mode/qos.py b/src/conf_mode/qos.py index ad4121a49..40d7a6c16 100755 --- a/src/conf_mode/qos.py +++ b/src/conf_mode/qos.py @@ -149,7 +149,7 @@ def verify(qos): if 'class' in policy_config: for cls, cls_config in policy_config['class'].items(): # bandwidth is not mandatory for priority-queue - that is why this is on the exception list - if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin']: + if 'bandwidth' not in cls_config and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError(f'Bandwidth must be defined for policy "{policy}" class "{cls}"!') if 'match' in cls_config: for match, match_config in cls_config['match'].items(): @@ -173,7 +173,7 @@ def verify(qos): if 'default' not in policy_config: raise ConfigError(f'Policy {policy} misses "default" class!') if 'default' in policy_config: - if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin']: + if 'bandwidth' not in policy_config['default'] and policy_type not in ['priority_queue', 'round_robin', 'shaper_hfsc']: raise ConfigError('Bandwidth not defined for default traffic!') # we should check interface ingress/egress configuration after verifying that diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 7ebc560ba..91ea354b6 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -31,6 +31,7 @@ from vyos.utils.file import chmod_775 from vyos.utils.file import makedir from vyos.utils.file import write_file from vyos.utils.process import call +from vyos.utils.network import interface_exists from vyos.utils.network import is_subnet_connected from vyos.utils.network import is_addr_assigned from vyos import ConfigError @@ -164,6 +165,7 @@ def verify(dhcp): shared_networks = len(dhcp['shared_network_name']) disabled_shared_networks = 0 + subnet_ids = [] # A shared-network requires a subnet definition for network, network_config in dhcp['shared_network_name'].items(): @@ -175,6 +177,14 @@ def verify(dhcp): 'lease subnet must be configured.') for subnet, subnet_config in network_config['subnet'].items(): + if 'subnet_id' not in subnet_config: + raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"') + + if subnet_config['subnet_id'] in subnet_ids: + raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique') + + subnet_ids.append(subnet_config['subnet_id']) + # All delivered static routes require a next-hop to be set if 'static_route' in subnet_config: for route, route_option in subnet_config['static_route'].items(): @@ -222,6 +232,9 @@ def verify(dhcp): if 'static_mapping' in subnet_config: # Static mappings require just a MAC address (will use an IP from the dynamic pool if IP is not set) + used_ips = [] + used_mac = [] + used_duid = [] for mapping, mapping_config in subnet_config['static_mapping'].items(): if 'ip_address' in mapping_config: if ip_address(mapping_config['ip_address']) not in ip_network(subnet): @@ -233,6 +246,22 @@ def verify(dhcp): raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') + if 'disable' not in mapping_config: + if mapping_config['ip_address'] in used_ips: + raise ConfigError(f'Configured IP address for static mapping "{mapping}" already exists on another static mapping') + used_ips.append(mapping_config['ip_address']) + + if 'disable' not in mapping_config: + if 'mac' in mapping_config: + if mapping_config['mac'] in used_mac: + raise ConfigError(f'Configured MAC address for static mapping "{mapping}" already exists on another static mapping') + used_mac.append(mapping_config['mac']) + + if 'duid' in mapping_config: + if mapping_config['duid'] in used_duid: + raise ConfigError(f'Configured DUID for static mapping "{mapping}" already exists on another static mapping') + used_duid.append(mapping_config['duid']) + # There must be one subnet connected to a listen interface. # This only counts if the network itself is not disabled! if 'disable' not in network_config: @@ -294,12 +323,18 @@ def verify(dhcp): else: raise ConfigError(f'listen-address "{address}" not configured on any interface') - if not listen_ok: raise ConfigError('None of the configured subnets have an appropriate primary IP address on any\n' 'broadcast interface configured, nor was there an explicit listen-address\n' 'configured for serving DHCP relay packets!') + if 'listen_address' in dhcp and 'listen_interface' in dhcp: + raise ConfigError(f'Cannot define listen-address and listen-interface at the same time') + + for interface in (dict_search('listen_interface', dhcp) or []): + if not interface_exists(interface): + raise ConfigError(f'listen-interface "{interface}" does not exist') + return None def generate(dhcp): diff --git a/src/conf_mode/service_dhcpv6-server.py b/src/conf_mode/service_dhcpv6-server.py index 9cc57dbcf..214531904 100755 --- a/src/conf_mode/service_dhcpv6-server.py +++ b/src/conf_mode/service_dhcpv6-server.py @@ -63,6 +63,7 @@ def verify(dhcpv6): # Inspect shared-network/subnet subnets = [] + subnet_ids = [] listen_ok = False for network, network_config in dhcpv6['shared_network_name'].items(): # A shared-network requires a subnet definition @@ -72,26 +73,37 @@ def verify(dhcpv6): 'each shared network!') for subnet, subnet_config in network_config['subnet'].items(): - if 'address_range' in subnet_config: - if 'start' in subnet_config['address_range']: - range6_start = [] - range6_stop = [] - for start, start_config in subnet_config['address_range']['start'].items(): - if 'stop' not in start_config: - raise ConfigError(f'address-range stop address for start "{start}" is not defined!') - stop = start_config['stop'] + if 'subnet_id' not in subnet_config: + raise ConfigError(f'Unique subnet ID not specified for subnet "{subnet}"') + + if subnet_config['subnet_id'] in subnet_ids: + raise ConfigError(f'Subnet ID for subnet "{subnet}" is not unique') + + subnet_ids.append(subnet_config['subnet_id']) + + if 'range' in subnet_config: + range6_start = [] + range6_stop = [] + + for num, range_config in subnet_config['range'].items(): + if 'start' in range_config: + start = range_config['start'] + + if 'stop' not in range_config: + raise ConfigError(f'Range stop address for start "{start}" is not defined!') + stop = range_config['stop'] # Start address must be inside network if not ip_address(start) in ip_network(subnet): - raise ConfigError(f'address-range start address "{start}" is not in subnet "{subnet}"!') + raise ConfigError(f'Range start address "{start}" is not in subnet "{subnet}"!') # Stop address must be inside network if not ip_address(stop) in ip_network(subnet): - raise ConfigError(f'address-range stop address "{stop}" is not in subnet "{subnet}"!') + raise ConfigError(f'Range stop address "{stop}" is not in subnet "{subnet}"!') # Stop address must be greater or equal to start address if not ip_address(stop) >= ip_address(start): - raise ConfigError(f'address-range stop address "{stop}" must be greater then or equal ' \ + raise ConfigError(f'Range stop address "{stop}" must be greater then or equal ' \ f'to the range start address "{start}"!') # DHCPv6 range start address must be unique - two ranges can't @@ -99,6 +111,7 @@ def verify(dhcpv6): if start in range6_start: raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool start address "{start}" defined multipe times!') + range6_start.append(start) # DHCPv6 range stop address must be unique - two ranges can't @@ -106,12 +119,14 @@ def verify(dhcpv6): if stop in range6_stop: raise ConfigError(f'Conflicting DHCPv6 lease range: '\ f'Pool stop address "{stop}" defined multipe times!') + range6_stop.append(stop) - if 'prefix' in subnet_config: - for prefix in subnet_config['prefix']: - if ip_network(prefix) not in ip_network(subnet): - raise ConfigError(f'address-range prefix "{prefix}" is not in subnet "{subnet}""') + if 'prefix' in range_config: + prefix = range_config['prefix'] + + if not ip_network(prefix).subnet_of(ip_network(subnet)): + raise ConfigError(f'Range prefix "{prefix}" is not in subnet "{subnet}"') # Prefix delegation sanity checks if 'prefix_delegation' in subnet_config: @@ -142,13 +157,15 @@ def verify(dhcpv6): raise ConfigError(f'Either MAC address or Client identifier (DUID) is required for ' f'static mapping "{mapping}" within shared-network "{network}, {subnet}"!') - if 'vendor_option' in subnet_config: - if len(dict_search('vendor_option.cisco.tftp_server', subnet_config)) > 2: - raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') + if 'option' in subnet_config: + if 'vendor_option' in subnet_config['option']: + if len(dict_search('option.vendor_option.cisco.tftp_server', subnet_config)) > 2: + raise ConfigError(f'No more then two Cisco tftp-servers should be defined for subnet "{subnet}"!') # Subnets must be unique if subnet in subnets: raise ConfigError(f'DHCPv6 subnets must be unique! Subnet {subnet} defined multiple times!') + subnets.append(subnet) # DHCPv6 requires at least one configured address range or one static mapping diff --git a/src/conf_mode/service_dns_dynamic.py b/src/conf_mode/service_dns_dynamic.py index 99fa8feee..a551a9891 100755 --- a/src/conf_mode/service_dns_dynamic.py +++ b/src/conf_mode/service_dns_dynamic.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -21,8 +21,10 @@ from sys import exit from vyos.base import Warning from vyos.config import Config from vyos.configverify import verify_interface_exists +from vyos.configverify import dynamic_interface_pattern from vyos.template import render from vyos.utils.process import call +from vyos.utils.network import interface_exists from vyos import ConfigError from vyos import airbag airbag.enable() @@ -30,9 +32,6 @@ airbag.enable() config_file = r'/run/ddclient/ddclient.conf' systemd_override = r'/run/systemd/system/ddclient.service.d/override.conf' -# Dynamic interfaces that might not exist when the configuration is loaded -dynamic_interfaces = ('pppoe', 'sstpc') - # Protocols that require zone zone_necessary = ['cloudflare', 'digitalocean', 'godaddy', 'hetzner', 'gandi', 'nfsn', 'nsupdate'] @@ -81,7 +80,6 @@ def verify(dyndns): # Dynamic DNS service provider - configuration validation for service, config in dyndns['name'].items(): - error_msg_req = f'is required for Dynamic DNS service "{service}"' error_msg_uns = f'is not supported for Dynamic DNS service "{service}"' @@ -89,29 +87,36 @@ def verify(dyndns): if field not in config: raise ConfigError(f'"{field.replace("_", "-")}" {error_msg_req}') - # If dyndns address is an interface, ensure - # that the interface exists (or just warn if dynamic interface) - # and that web-options are not set - if config['address'] != 'web': + if not any(x in config['address'] for x in ['interface', 'web']): + raise ConfigError(f'Either "interface" or "web" {error_msg_req} ' + f'with protocol "{config["protocol"]}"') + if all(x in config['address'] for x in ['interface', 'web']): + raise ConfigError(f'Both "interface" and "web" at the same time {error_msg_uns} ' + f'with protocol "{config["protocol"]}"') + + # If dyndns address is an interface, ensure that the interface exists + # and warn if a non-active dynamic interface is used + if 'interface' in config['address']: + tmp = re.compile(dynamic_interface_pattern) # exclude check interface for dynamic interfaces - if config['address'].startswith(dynamic_interfaces): - Warning(f'Interface "{config["address"]}" does not exist yet and cannot ' - f'be used for Dynamic DNS service "{service}" until it is up!') + if tmp.match(config['address']['interface']): + if not interface_exists(config['address']['interface']): + Warning(f'Interface "{config["address"]["interface"]}" does not exist yet and ' + f'cannot be used for Dynamic DNS service "{service}" until it is up!') else: - verify_interface_exists(config['address']) - if 'web_options' in config: - raise ConfigError(f'"web-options" is applicable only when using HTTP(S) ' - f'web request to obtain the IP address') - - # Warn if using checkip.dyndns.org, as it does not support HTTPS - # See: https://github.com/ddclient/ddclient/issues/597 - if 'web_options' in config: - if 'url' not in config['web_options']: - raise ConfigError(f'"url" in "web-options" {error_msg_req} ' + verify_interface_exists(config['address']['interface']) + + if 'web' in config['address']: + # If 'skip' is specified, 'url' is required as well + if 'skip' in config['address']['web'] and 'url' not in config['address']['web']: + raise ConfigError(f'"url" along with "skip" {error_msg_req} ' f'with protocol "{config["protocol"]}"') - elif re.search("^(https?://)?checkip\.dyndns\.org", config['web_options']['url']): - Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address ' - f'lookup. Please use a different IP address lookup service.') + if 'url' in config['address']['web']: + # Warn if using checkip.dyndns.org, as it does not support HTTPS + # See: https://github.com/ddclient/ddclient/issues/597 + if re.search("^(https?://)?checkip\.dyndns\.org", config['address']['web']['url']): + Warning(f'"checkip.dyndns.org" does not support HTTPS requests for IP address ' + f'lookup. Please use a different IP address lookup service.') # RFC2136 uses 'key' instead of 'password' if config['protocol'] != 'nsupdate' and 'password' not in config: diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py index c186f47af..ecad765f4 100755 --- a/src/conf_mode/service_dns_forwarding.py +++ b/src/conf_mode/service_dns_forwarding.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -26,18 +26,18 @@ from vyos.template import render from vyos.template import bracketize_ipv6 from vyos.utils.process import call from vyos.utils.permission import chown -from vyos.utils.dict import dict_search from vyos import ConfigError from vyos import airbag airbag.enable() -pdns_rec_user = pdns_rec_group = 'pdns' -pdns_rec_run_dir = '/run/powerdns' +pdns_rec_user_group = 'pdns' +pdns_rec_run_dir = '/run/pdns-recursor' pdns_rec_lua_conf_file = f'{pdns_rec_run_dir}/recursor.conf.lua' pdns_rec_hostsd_lua_conf_file = f'{pdns_rec_run_dir}/recursor.vyos-hostsd.conf.lua' pdns_rec_hostsd_zones_file = f'{pdns_rec_run_dir}/recursor.forward-zones.conf' pdns_rec_config_file = f'{pdns_rec_run_dir}/recursor.conf' +pdns_rec_systemd_override = '/run/systemd/system/pdns-recursor.service.d/override.conf' hostsd_tag = 'static' @@ -55,6 +55,9 @@ def get_config(config=None): get_first_key=True, with_recursive_defaults=True) + dns['config_file'] = pdns_rec_config_file + dns['config_dir'] = os.path.dirname(pdns_rec_config_file) + # some additions to the default dictionary if 'system' in dns: base_nameservers = ['system', 'name-server'] @@ -251,11 +254,16 @@ def generate(dns): if not dns: return None - render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2', - dns, user=pdns_rec_user, group=pdns_rec_group) + render(pdns_rec_systemd_override, 'dns-forwarding/override.conf.j2', dns) + + render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2', dns, + user=pdns_rec_user_group, group=pdns_rec_user_group) - render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2', - dns, user=pdns_rec_user, group=pdns_rec_group) + render(pdns_rec_config_file, 'dns-forwarding/recursor.conf.j2', dns, + user=pdns_rec_user_group, group=pdns_rec_user_group) + + render(pdns_rec_lua_conf_file, 'dns-forwarding/recursor.conf.lua.j2', dns, + user=pdns_rec_user_group, group=pdns_rec_user_group) for zone_filename in glob(f'{pdns_rec_run_dir}/zone.*.conf'): os.unlink(zone_filename) @@ -263,21 +271,25 @@ def generate(dns): if 'authoritative_zones' in dns: for zone in dns['authoritative_zones']: render(zone['file'], 'dns-forwarding/recursor.zone.conf.j2', - zone, user=pdns_rec_user, group=pdns_rec_group) + zone, user=pdns_rec_user_group, group=pdns_rec_user_group) # if vyos-hostsd didn't create its files yet, create them (empty) for file in [pdns_rec_hostsd_lua_conf_file, pdns_rec_hostsd_zones_file]: with open(file, 'a'): pass - chown(file, user=pdns_rec_user, group=pdns_rec_group) + chown(file, user=pdns_rec_user_group, group=pdns_rec_user_group) return None def apply(dns): + systemd_service = 'pdns-recursor.service' + # Reload systemd manager configuration + call('systemctl daemon-reload') + if not dns: # DNS forwarding is removed in the commit - call('systemctl stop pdns-recursor.service') + call(f'systemctl stop {systemd_service}') if os.path.isfile(pdns_rec_config_file): os.unlink(pdns_rec_config_file) @@ -345,7 +357,7 @@ def apply(dns): hc.apply() ### finally (re)start pdns-recursor - call('systemctl restart pdns-recursor.service') + call(f'systemctl reload-or-restart {systemd_service}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py index cb40acc9f..46efc3c93 100755 --- a/src/conf_mode/service_https.py +++ b/src/conf_mode/service_https.py @@ -15,56 +15,41 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. import os +import socket import sys import json -from copy import deepcopy from time import sleep -import vyos.defaults -import vyos.certbot_util - from vyos.base import Warning from vyos.config import Config +from vyos.config import config_dict_merge from vyos.configdiff import get_config_diff from vyos.configverify import verify_vrf -from vyos import ConfigError +from vyos.defaults import api_config_state from vyos.pki import wrap_certificate from vyos.pki import wrap_private_key +from vyos.pki import wrap_dh_parameters +from vyos.pki import load_dh_parameters from vyos.template import render +from vyos.utils.dict import dict_search from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running from vyos.utils.process import is_systemd_service_active from vyos.utils.network import check_port_availability from vyos.utils.network import is_listen_port_bind_service from vyos.utils.file import write_file - +from vyos import ConfigError from vyos import airbag airbag.enable() -config_file = '/etc/nginx/sites-available/default' +config_file = '/etc/nginx/sites-enabled/default' systemd_override = r'/run/systemd/system/nginx.service.d/override.conf' -cert_dir = '/etc/ssl/certs' -key_dir = '/etc/ssl/private' -certbot_dir = vyos.defaults.directories['certbot'] - -api_config_state = '/run/http-api-state' -systemd_service = '/run/systemd/system/vyos-http-api.service' - -# https config needs to coordinate several subsystems: api, certbot, -# self-signed certificate, as well as the virtual hosts defined within the -# https config definition itself. Consequently, one needs a general dict, -# encompassing the https and other configs, and a list of such virtual hosts -# (server blocks in nginx terminology) to pass to the jinja2 template. -default_server_block = { - 'id' : '', - 'address' : '*', - 'port' : '443', - 'name' : ['_'], - 'api' : False, - 'vyos_cert' : {}, - 'certbot' : False -} +cert_dir = '/run/nginx/certs' + +user = 'www-data' +group = 'www-data' + +systemd_service_api = '/run/systemd/system/vyos-http-api.service' def get_config(config=None): if config: @@ -76,93 +61,70 @@ def get_config(config=None): if not conf.exists(base): return None - diff = get_config_diff(conf) - - https = conf.get_config_dict(base, get_first_key=True, with_pki=True) - - https['children_changed'] = diff.node_changed_children(base) - https['api_add_or_delete'] = diff.node_changed_presence(base + ['api']) + https = conf.get_config_dict(base, get_first_key=True, + key_mangling=('-', '_'), + with_pki=True) - if 'api' not in https: - return https + # store path to API config file for later use in templates + https['api_config_state'] = api_config_state + # get fully qualified system hsotname + https['hostname'] = socket.getfqdn() - http_api = conf.get_config_dict(base + ['api'], key_mangling=('-', '_'), - no_tag_node_value_mangle=True, - get_first_key=True, - with_recursive_defaults=True) - - if http_api.from_defaults(['graphql']): - del http_api['graphql'] - - # Do we run inside a VRF context? - vrf_path = ['service', 'https', 'vrf'] - if conf.exists(vrf_path): - http_api['vrf'] = conf.return_value(vrf_path) - - https['api'] = http_api + # We have gathered the dict representation of the CLI, but there are default + # options which we need to update into the dictionary retrived. + default_values = conf.get_config_defaults(**https.kwargs, recursive=True) + if 'api' not in https or 'graphql' not in https['api']: + del default_values['api'] + # merge CLI and default dictionary + https = config_dict_merge(default_values, https) return https def verify(https): - from vyos.utils.dict import dict_search - if https is None: return None - if 'certificates' in https: - certificates = https['certificates'] + if 'certificates' in https and 'certificate' in https['certificates']: + cert_name = https['certificates']['certificate'] + if 'pki' not in https: + raise ConfigError('PKI is not configured!') - if 'certificate' in certificates: - if not https['pki']: - raise ConfigError('PKI is not configured') + if cert_name not in https['pki']['certificate']: + raise ConfigError('Invalid certificate in configuration!') - cert_name = certificates['certificate'] - - if cert_name not in https['pki']['certificate']: - raise ConfigError("Invalid certificate on https configuration") + pki_cert = https['pki']['certificate'][cert_name] - pki_cert = https['pki']['certificate'][cert_name] + if 'certificate' not in pki_cert: + raise ConfigError('Missing certificate in configuration!') - if 'certificate' not in pki_cert: - raise ConfigError("Missing certificate on https configuration") + if 'private' not in pki_cert or 'key' not in pki_cert['private']: + raise ConfigError('Missing certificate private key in configuration!') - if 'private' not in pki_cert or 'key' not in pki_cert['private']: - raise ConfigError("Missing certificate private key on https configuration") + if 'dh_params' in https['certificates']: + dh_name = https['certificates']['dh_params'] + if dh_name not in https['pki']['dh']: + raise ConfigError('Invalid DH parameter in configuration!') - if 'certbot' in https['certificates']: - vhost_names = [] - for _, vh_conf in https.get('virtual-host', {}).items(): - vhost_names += vh_conf.get('server-name', []) - domains = https['certificates']['certbot'].get('domain-name', []) - domains_found = [domain for domain in domains if domain in vhost_names] - if not domains_found: - raise ConfigError("At least one 'virtual-host <id> server-name' " - "matching the 'certbot domain-name' is required.") + pki_dh = https['pki']['dh'][dh_name] + dh_params = load_dh_parameters(pki_dh['parameters']) + dh_numbers = dh_params.parameter_numbers() + dh_bits = dh_numbers.p.bit_length() + if dh_bits < 2048: + raise ConfigError(f'Minimum DH key-size is 2048 bits') - server_block_list = [] + else: + Warning('No certificate specified, using build-in self-signed certificates. '\ + 'Do not use them in a production environment!') - # organize by vhosts - vhost_dict = https.get('virtual-host', {}) + # Check if server port is already in use by a different appliaction + listen_address = ['0.0.0.0'] + port = int(https['port']) + if 'listen_address' in https: + listen_address = https['listen_address'] - if not vhost_dict: - # no specified virtual hosts (server blocks); use default - server_block_list.append(default_server_block) - else: - for vhost in list(vhost_dict): - server_block = deepcopy(default_server_block) - data = vhost_dict.get(vhost, {}) - server_block['address'] = data.get('listen-address', '*') - server_block['port'] = data.get('port', '443') - server_block_list.append(server_block) - - for entry in server_block_list: - _address = entry.get('address') - _address = '0.0.0.0' if _address == '*' else _address - _port = entry.get('port') - proto = 'tcp' - if check_port_availability(_address, int(_port), proto) is not True and \ - not is_listen_port_bind_service(int(_port), 'nginx'): - raise ConfigError(f'"{proto}" port "{_port}" is used by another service') + for address in listen_address: + if not check_port_availability(address, port, 'tcp') and not is_listen_port_bind_service(port, 'nginx'): + raise ConfigError(f'TCP port "{port}" is used by another service!') verify_vrf(https) @@ -187,106 +149,61 @@ def verify(https): # If only key-based methods are enabled, # fail the commit if no valid key configurations are found if (not valid_keys_exist) and (not jwt_auth): - raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled') + raise ConfigError('At least one HTTPS API key is required unless GraphQL token authentication is enabled!') if (not valid_keys_exist) and jwt_auth: - Warning(f'API keys are not configured: the classic (non-GraphQL) API will be unavailable.') + Warning(f'API keys are not configured: classic (non-GraphQL) API will be unavailable!') return None def generate(https): if https is None: + for file in [systemd_service_api, config_file, systemd_override]: + if os.path.exists(file): + os.unlink(file) return None - if 'api' not in https: - if os.path.exists(systemd_service): - os.unlink(systemd_service) - else: - render(systemd_service, 'https/vyos-http-api.service.j2', https['api']) + if 'api' in https: + render(systemd_service_api, 'https/vyos-http-api.service.j2', https) with open(api_config_state, 'w') as f: json.dump(https['api'], f, indent=2) - - server_block_list = [] - - # organize by vhosts - - vhost_dict = https.get('virtual-host', {}) - - if not vhost_dict: - # no specified virtual hosts (server blocks); use default - server_block_list.append(default_server_block) else: - for vhost in list(vhost_dict): - server_block = deepcopy(default_server_block) - server_block['id'] = vhost - data = vhost_dict.get(vhost, {}) - server_block['address'] = data.get('listen-address', '*') - server_block['port'] = data.get('port', '443') - name = data.get('server-name', ['_']) - server_block['name'] = name - allow_client = data.get('allow-client', {}) - server_block['allow_client'] = allow_client.get('address', []) - server_block_list.append(server_block) + if os.path.exists(systemd_service_api): + os.unlink(systemd_service_api) # get certificate data - - cert_dict = https.get('certificates', {}) - - if 'certificate' in cert_dict: - cert_name = cert_dict['certificate'] + if 'certificates' in https and 'certificate' in https['certificates']: + cert_name = https['certificates']['certificate'] pki_cert = https['pki']['certificate'][cert_name] - cert_path = os.path.join(cert_dir, f'{cert_name}.pem') - key_path = os.path.join(key_dir, f'{cert_name}.pem') + cert_path = os.path.join(cert_dir, f'{cert_name}_cert.pem') + key_path = os.path.join(cert_dir, f'{cert_name}_key.pem') server_cert = str(wrap_certificate(pki_cert['certificate'])) - if 'ca-certificate' in cert_dict: - ca_cert = cert_dict['ca-certificate'] + + # Append CA certificate if specified to form a full chain + if 'ca_certificate' in https['certificates']: + ca_cert = https['certificates']['ca_certificate'] server_cert += '\n' + str(wrap_certificate(https['pki']['ca'][ca_cert]['certificate'])) - write_file(cert_path, server_cert) - write_file(key_path, wrap_private_key(pki_cert['private']['key'])) - - vyos_cert_data = { - 'crt': cert_path, - 'key': key_path - } - - for block in server_block_list: - block['vyos_cert'] = vyos_cert_data - - # letsencrypt certificate using certbot - - certbot = False - cert_domains = cert_dict.get('certbot', {}).get('domain-name', []) - if cert_domains: - certbot = True - for domain in cert_domains: - sub_list = vyos.certbot_util.choose_server_block(server_block_list, - domain) - if sub_list: - for sb in sub_list: - sb['certbot'] = True - sb['certbot_dir'] = certbot_dir - # certbot organizes certificates by first domain - sb['certbot_domain_dir'] = cert_domains[0] - - if 'api' in list(https): - vhost_list = https.get('api-restrict', {}).get('virtual-host', []) - if not vhost_list: - for block in server_block_list: - block['api'] = True - else: - for block in server_block_list: - if block['id'] in vhost_list: - block['api'] = True - - data = { - 'server_block_list': server_block_list, - 'certbot': certbot - } - - render(config_file, 'https/nginx.default.j2', data) + write_file(cert_path, server_cert, user=user, group=group, mode=0o644) + write_file(key_path, wrap_private_key(pki_cert['private']['key']), + user=user, group=group, mode=0o600) + + tmp_path = {'cert_path': cert_path, 'key_path': key_path} + + if 'dh_params' in https['certificates']: + dh_name = https['certificates']['dh_params'] + pki_dh = https['pki']['dh'][dh_name] + if 'parameters' in pki_dh: + dh_path = os.path.join(cert_dir, f'{dh_name}_dh.pem') + write_file(dh_path, wrap_dh_parameters(pki_dh['parameters']), + user=user, group=group, mode=0o600) + tmp_path.update({'dh_file' : dh_path}) + + https['certificates'].update(tmp_path) + + render(config_file, 'https/nginx.default.j2', https) render(systemd_override, 'https/override.conf.j2', https) return None @@ -297,27 +214,18 @@ def apply(https): https_service_name = 'nginx.service' if https is None: - if is_systemd_service_active(f'{http_api_service_name}'): - call(f'systemctl stop {http_api_service_name}') + call(f'systemctl stop {http_api_service_name}') call(f'systemctl stop {https_service_name}') return - if 'api' in https['children_changed']: - if 'api' in https: - if is_systemd_service_running(f'{http_api_service_name}'): - call(f'systemctl reload {http_api_service_name}') - else: - call(f'systemctl restart {http_api_service_name}') - # Let uvicorn settle before (possibly) restarting nginx - sleep(1) - else: - if is_systemd_service_active(f'{http_api_service_name}'): - call(f'systemctl stop {http_api_service_name}') - - if (not is_systemd_service_running(f'{https_service_name}') or - https['api_add_or_delete'] or - set(https['children_changed']) - set(['api'])): - call(f'systemctl restart {https_service_name}') + if 'api' in https: + call(f'systemctl reload-or-restart {http_api_service_name}') + # Let uvicorn settle before (possibly) restarting nginx + sleep(1) + elif is_systemd_service_active(http_api_service_name): + call(f'systemctl stop {http_api_service_name}') + + call(f'systemctl reload-or-restart {https_service_name}') if __name__ == '__main__': try: diff --git a/src/conf_mode/service_https_certificates_certbot.py b/src/conf_mode/service_https_certificates_certbot.py deleted file mode 100755 index 1a6a498de..000000000 --- a/src/conf_mode/service_https_certificates_certbot.py +++ /dev/null @@ -1,114 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2019-2020 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import sys -import os - -import vyos.defaults -from vyos.config import Config -from vyos import ConfigError -from vyos.utils.process import cmd -from vyos.utils.process import call -from vyos.utils.process import is_systemd_service_running - -from vyos import airbag -airbag.enable() - -vyos_conf_scripts_dir = vyos.defaults.directories['conf_mode'] -vyos_certbot_dir = vyos.defaults.directories['certbot'] - -dependencies = [ - 'service_https.py', -] - -def request_certbot(cert): - email = cert.get('email') - if email is not None: - email_flag = '-m {0}'.format(email) - else: - email_flag = '' - - domains = cert.get('domains') - if domains is not None: - domain_flag = '-d ' + ' -d '.join(domains) - else: - domain_flag = '' - - certbot_cmd = f'certbot certonly --config-dir {vyos_certbot_dir} -n --nginx --agree-tos --no-eff-email --expand {email_flag} {domain_flag}' - - cmd(certbot_cmd, - raising=ConfigError, - message="The certbot request failed for the specified domains.") - -def get_config(): - conf = Config() - if not conf.exists('service https certificates certbot'): - return None - else: - conf.set_level('service https certificates certbot') - - cert = {} - - if conf.exists('domain-name'): - cert['domains'] = conf.return_values('domain-name') - - if conf.exists('email'): - cert['email'] = conf.return_value('email') - - return cert - -def verify(cert): - if cert is None: - return None - - if 'domains' not in cert: - raise ConfigError("At least one domain name is required to" - " request a letsencrypt certificate.") - - if 'email' not in cert: - raise ConfigError("An email address is required to request" - " a letsencrypt certificate.") - -def generate(cert): - if cert is None: - return None - - # certbot will attempt to reload nginx, even with 'certonly'; - # start nginx if not active - if not is_systemd_service_running('nginx.service'): - call('systemctl start nginx.service') - - request_certbot(cert) - -def apply(cert): - if cert is not None: - call('systemctl restart certbot.timer') - else: - call('systemctl stop certbot.timer') - return None - - for dep in dependencies: - cmd(f'{vyos_conf_scripts_dir}/{dep}', raising=ConfigError) - -if __name__ == '__main__': - try: - c = get_config() - verify(c) - generate(c) - apply(c) - except ConfigError as e: - print(e) - sys.exit(1) diff --git a/src/conf_mode/service_ipoe-server.py b/src/conf_mode/service_ipoe-server.py index 36f00dec5..6df6f3dc7 100755 --- a/src/conf_mode/service_ipoe-server.py +++ b/src/conf_mode/service_ipoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -79,9 +79,6 @@ def verify(ipoe): if 'key' not in radius_config: raise ConfigError(f'Missing RADIUS secret key for server "{server}"') - if 'client_ipv6_pool' in ipoe: - if 'delegate' in ipoe['client_ipv6_pool'] and 'prefix' not in ipoe['client_ipv6_pool']: - raise ConfigError('IPoE IPv6 deletate-prefix requires IPv6 prefix to be configured!') return None diff --git a/src/conf_mode/service_ntp.py b/src/conf_mode/service_ntp.py index 1cc23a7df..f11690ee6 100755 --- a/src/conf_mode/service_ntp.py +++ b/src/conf_mode/service_ntp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -42,7 +42,7 @@ def get_config(config=None): if not conf.exists(base): return None - ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True) + ntp = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, with_defaults=True) ntp['config_file'] = config_file ntp['user'] = user_group diff --git a/src/conf_mode/service_pppoe-server.py b/src/conf_mode/service_pppoe-server.py index 7c624f034..31299a15c 100755 --- a/src/conf_mode/service_pppoe-server.py +++ b/src/conf_mode/service_pppoe-server.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2023 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -85,6 +85,7 @@ def verify(pppoe): if not dict_search('authentication.radius.dynamic_author.key', pppoe): raise ConfigError('DA/CoE server key required!') + return None diff --git a/src/conf_mode/system_console.py b/src/conf_mode/system_console.py index ebf9a113b..a888b125e 100755 --- a/src/conf_mode/system_console.py +++ b/src/conf_mode/system_console.py @@ -22,6 +22,7 @@ from vyos.config import Config from vyos.utils.process import call from vyos.utils.file import read_file from vyos.utils.file import write_file +from vyos.system import grub_util from vyos.template import render from vyos import ConfigError from vyos import airbag @@ -114,30 +115,7 @@ def generate(console): return None speed = console['device']['ttyS0']['speed'] - grub_config = '/boot/grub/grub.cfg' - if not os.path.isfile(grub_config): - return None - - lines = read_file(grub_config).split('\n') - p = re.compile(r'^(.* console=ttyS0),[0-9]+(.*)$') - write = False - newlines = [] - for line in lines: - if line.startswith('serial --unit'): - newline = f'serial --unit=0 --speed={speed}' - elif p.match(line): - newline = '{},{}{}'.format(p.search(line)[1], speed, p.search(line)[2]) - else: - newline = line - - if newline != line: - write = True - - newlines.append(newline) - newlines.append('') - - if write: - write_file(grub_config, '\n'.join(newlines)) + grub_util.update_console_speed(speed) return None diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py index d92121b3d..3b5b67437 100755 --- a/src/conf_mode/system_option.py +++ b/src/conf_mode/system_option.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -22,6 +22,7 @@ from time import sleep from vyos.config import Config from vyos.configverify import verify_source_interface +from vyos.system import grub_util from vyos.template import render from vyos.utils.process import cmd from vyos.utils.process import is_systemd_service_running @@ -39,7 +40,6 @@ time_format_to_locale = { '24-hour': 'en_GB.UTF-8' } - def get_config(config=None): if config: conf = config @@ -87,6 +87,13 @@ def verify(options): def generate(options): render(curlrc_config, 'system/curlrc.j2', options) render(ssh_config, 'system/ssh_config.j2', options) + + cmdline_options = [] + if 'kernel' in options: + if 'disable_mitigations' in options['kernel']: + cmdline_options.append('mitigations=off') + grub_util.update_kernel_cmdline_options(' '.join(cmdline_options)) + return None def apply(options): diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index 2df1bbb7a..41119b494 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -19,6 +19,7 @@ import os from sys import exit from vyos.config import Config +from vyos.configverify import verify_vrf from vyos.template import render from vyos.utils.process import call from vyos.utils.network import is_addr_assigned @@ -46,7 +47,6 @@ def get_config(config=None): return sflow - def verify(sflow): if not sflow: return None @@ -68,9 +68,8 @@ def verify(sflow): if 'server' not in sflow: raise ConfigError('You need to configure at least one sFlow server!') - # return True if all checks were passed - return True - + verify_vrf(sflow) + return None def generate(sflow): if not sflow: @@ -81,7 +80,6 @@ def generate(sflow): # Reload systemd manager configuration call('systemctl daemon-reload') - def apply(sflow): if not sflow: # Stop flow-accounting daemon and remove configuration file @@ -93,7 +91,6 @@ def apply(sflow): # Start/reload flow-accounting daemon call(f'systemctl restart {systemd_service}') - if __name__ == '__main__': try: config = get_config() diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py index 7fd32c230..d074ed159 100755 --- a/src/conf_mode/vpn_ipsec.py +++ b/src/conf_mode/vpn_ipsec.py @@ -27,6 +27,7 @@ from vyos.base import Warning from vyos.config import Config from vyos.configdict import leaf_node_changed from vyos.configverify import verify_interface_exists +from vyos.configverify import dynamic_interface_pattern from vyos.defaults import directories from vyos.ifconfig import Interface from vyos.pki import encode_certificate @@ -43,6 +44,7 @@ from vyos.template import is_ipv4 from vyos.template import is_ipv6 from vyos.template import render from vyos.utils.network import is_ipv6_link_local +from vyos.utils.network import interface_exists from vyos.utils.dict import dict_search from vyos.utils.dict import dict_search_args from vyos.utils.process import call @@ -65,11 +67,11 @@ default_install_routes = 'yes' vici_socket = '/var/run/charon.vici' -CERT_PATH = f'{swanctl_dir}/x509/' +CERT_PATH = f'{swanctl_dir}/x509/' PUBKEY_PATH = f'{swanctl_dir}/pubkey/' -KEY_PATH = f'{swanctl_dir}/private/' -CA_PATH = f'{swanctl_dir}/x509ca/' -CRL_PATH = f'{swanctl_dir}/x509crl/' +KEY_PATH = f'{swanctl_dir}/private/' +CA_PATH = f'{swanctl_dir}/x509ca/' +CRL_PATH = f'{swanctl_dir}/x509crl/' DHCP_HOOK_IFLIST = '/tmp/ipsec_dhcp_waiting' @@ -158,9 +160,16 @@ def verify(ipsec): if 'id' not in psk_config or 'secret' not in psk_config: raise ConfigError(f'Authentication psk "{psk}" missing "id" or "secret"') - if 'interfaces' in ipsec : - for ifname in ipsec['interface']: - verify_interface_exists(ifname) + if 'interface' in ipsec: + tmp = re.compile(dynamic_interface_pattern) + for interface in ipsec['interface']: + # exclude check interface for dynamic interfaces + if tmp.match(interface): + if not interface_exists(interface): + Warning(f'Interface "{interface}" does not exist yet and cannot be used ' + f'for IPsec until it is up!') + else: + verify_interface_exists(interface) if 'l2tp' in ipsec: if 'esp_group' in ipsec['l2tp']: @@ -394,7 +403,7 @@ def verify(ipsec): if 'bind' in peer_conf['vti']: vti_interface = peer_conf['vti']['bind'] - if not os.path.exists(f'/sys/class/net/{vti_interface}'): + if not interface_exists(vti_interface): raise ConfigError(f'VTI interface {vti_interface} for site-to-site peer {peer} does not exist!') if 'vti' not in peer_conf and 'tunnel' not in peer_conf: diff --git a/src/conf_mode/vpn_l2tp.py b/src/conf_mode/vpn_l2tp.py index 03a27d3cd..b569ca140 100755 --- a/src/conf_mode/vpn_l2tp.py +++ b/src/conf_mode/vpn_l2tp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -70,15 +70,9 @@ def verify(l2tp): if not dict_search('authentication.radius.dynamic_author.key', l2tp): raise ConfigError('DA/CoE server key required!') - if dict_search('authentication.mode', l2tp) in ['local', 'noauth']: - if not l2tp['client_ip_pool'] and not l2tp['client_ipv6_pool']: - raise ConfigError( - "L2TP local auth mode requires local client-ip-pool or client-ipv6-pool to be configured!") - if dict_search('client_ip_pool', l2tp) and not dict_search('default_pool', l2tp): - Warning("'default-pool' is not defined") - verify_accel_ppp_ip_pool(l2tp) + if 'wins_server' in l2tp and len(l2tp['wins_server']) > 2: raise ConfigError( 'Not more then two WINS name-servers can be configured') diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py index f769be39f..0629625bf 100755 --- a/src/conf_mode/vpn_pptp.py +++ b/src/conf_mode/vpn_pptp.py @@ -80,12 +80,6 @@ def verify(pptp): raise ConfigError( f'Missing RADIUS secret key for server "{server}"') - if auth_mode == 'local' or auth_mode == 'noauth': - if not dict_search('client_ip_pool', pptp): - raise ConfigError( - 'PPTP local auth mode requires local client-ip-pool ' - 'to be configured!') - verify_accel_ppp_ip_pool(pptp) if 'name_server' in pptp: diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py index 6bf9307e1..a84513a0f 100755 --- a/src/conf_mode/vpn_sstp.py +++ b/src/conf_mode/vpn_sstp.py @@ -74,11 +74,8 @@ def verify(sstp): raise ConfigError(f'"{proto}" port "{port}" is used by another service') verify_accel_ppp_base_service(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/conf_mode/vrf.py b/src/conf_mode/vrf.py index 9b1b6355f..f2c544aa6 100755 --- a/src/conf_mode/vrf.py +++ b/src/conf_mode/vrf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2023 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -27,13 +27,12 @@ from vyos.ifconfig import Interface from vyos.template import render from vyos.template import render_to_string from vyos.utils.dict import dict_search +from vyos.utils.kernel import check_kmod from vyos.utils.network import get_interface_config from vyos.utils.network import get_vrf_members from vyos.utils.network import interface_exists from vyos.utils.process import call from vyos.utils.process import cmd -from vyos.utils.process import popen -from vyos.utils.process import run from vyos.utils.system import sysctl_write from vyos import ConfigError from vyos import frr @@ -41,17 +40,29 @@ from vyos import airbag airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-vrf.conf' -nft_vrf_config = '/tmp/nftables-vrf-zones' - -def has_rule(af : str, priority : int, table : str): - """ Check if a given ip rule exists """ +k_mod = ['vrf'] + +def has_rule(af : str, priority : int, table : str=None): + """ + Check if a given ip rule exists + $ ip --json -4 rule show + [{'l3mdev': None, 'priority': 1000, 'src': 'all'}, + {'action': 'unreachable', 'l3mdev': None, 'priority': 2000, 'src': 'all'}, + {'priority': 32765, 'src': 'all', 'table': 'local'}, + {'priority': 32766, 'src': 'all', 'table': 'main'}, + {'priority': 32767, 'src': 'all', 'table': 'default'}] + """ if af not in ['-4', '-6']: raise ValueError() - command = f'ip -j {af} rule show' + command = f'ip --detail --json {af} rule show' for tmp in loads(cmd(command)): - if {'priority', 'table'} <= set(tmp): + if 'priority' in tmp and 'table' in tmp: if tmp['priority'] == priority and tmp['table'] == table: return True + elif 'priority' in tmp and table in tmp: + # l3mdev table has a different layout + if tmp['priority'] == priority: + return True return False def vrf_interfaces(c, match): @@ -173,8 +184,6 @@ def verify(vrf): def generate(vrf): # Render iproute2 VR helper names render(config_file, 'iproute2/vrf.conf.j2', vrf) - # Render nftables zones config - render(nft_vrf_config, 'firewall/nftables-vrf-zones.j2', vrf) # Render VRF Kernel/Zebra route-map filters vrf['frr_zebra_config'] = render_to_string('frr/zebra.vrf.route-map.frr.j2', vrf) @@ -227,14 +236,6 @@ def apply(vrf): sysctl_write('net.vrf.strict_mode', strict_mode) if 'name' in vrf: - # Separate VRFs in conntrack table - # check if table already exists - _, err = popen('nft list table inet vrf_zones') - # If not, create a table - if err and os.path.exists(nft_vrf_config): - cmd(f'nft -f {nft_vrf_config}') - os.unlink(nft_vrf_config) - # Linux routing uses rules to find tables - routing targets are then # looked up in those tables. If the lookup got a matching route, the # process ends. @@ -318,17 +319,11 @@ def apply(vrf): frr_cfg.add_before(frr.default_add_before, vrf['frr_zebra_config']) frr_cfg.commit_configuration(zebra_daemon) - # return to default lookup preference when no VRF is configured - if 'name' not in vrf: - # Remove VRF zones table from nftables - tmp = run('nft list table inet vrf_zones') - if tmp == 0: - cmd('nft delete table inet vrf_zones') - return None if __name__ == '__main__': try: + check_kmod(k_mod) c = get_config() verify(c) generate(c) diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/03-vyos-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyos-dhclient-hook index 35721d009..d5e6462ba 100644 --- a/src/etc/dhcp/dhclient-exit-hooks.d/03-vyos-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/03-vyos-dhclient-hook @@ -29,6 +29,7 @@ fi if [ "$RUN" = "yes" ]; then BASE_PATH=$(python3 -c "from vyos.defaults import directories; print(directories['isc_dhclient_dir'])") + mkdir -p ${BASE_PATH} LOG=${BASE_PATH}/dhclient_"$interface"."$proto"lease echo `date` > $LOG diff --git a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook index c7a92fe26..e6edc1ac3 100755 --- a/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook +++ b/src/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook @@ -71,10 +71,6 @@ if __name__ == '__main__': conf_lines[i] = line.replace(old_ip, new_ip) found = True - for i, line in enumerate(secrets_lines): - if line.find(to_match) > 0: - secrets_lines[i] = line.replace(old_ip, new_ip) - if found: write_file(SWANCTL_CONF, conf_lines) ipsec_down(old_ip) diff --git a/src/etc/systemd/system/certbot.service.d/10-override.conf b/src/etc/systemd/system/certbot.service.d/10-override.conf new file mode 100644 index 000000000..542f77eb2 --- /dev/null +++ b/src/etc/systemd/system/certbot.service.d/10-override.conf @@ -0,0 +1,7 @@ +[Unit] +After= +After=vyos-router.service + +[Service] +ExecStart= +ExecStart=/usr/bin/certbot renew --config-dir /config/auth/letsencrypt --no-random-sleep-on-renew --post-hook "/usr/libexec/vyos/vyos-certbot-renew-pki.sh" diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf index 094f83551..614b4f7ed 100644 --- a/src/etc/systemd/system/frr.service.d/override.conf +++ b/src/etc/systemd/system/frr.service.d/override.conf @@ -1,3 +1,6 @@ +[Unit] +After=vyos-router.service + [Service] LimitNOFILE=4096 ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \ diff --git a/src/etc/systemd/system/nginx.service.d/10-override.conf b/src/etc/systemd/system/nginx.service.d/10-override.conf new file mode 100644 index 000000000..1be5cec81 --- /dev/null +++ b/src/etc/systemd/system/nginx.service.d/10-override.conf @@ -0,0 +1,3 @@ +[Unit] +After= +After=vyos-router.service diff --git a/src/etc/systemd/system/pdns-recursor.service.d/override.conf b/src/etc/systemd/system/pdns-recursor.service.d/override.conf deleted file mode 100644 index 158bac02b..000000000 --- a/src/etc/systemd/system/pdns-recursor.service.d/override.conf +++ /dev/null @@ -1,8 +0,0 @@ -[Service] -WorkingDirectory= -WorkingDirectory=/run/powerdns -RuntimeDirectory= -RuntimeDirectory=powerdns -RuntimeDirectoryPreserve=yes -ExecStart= -ExecStart=/usr/sbin/pdns_recursor --daemon=no --write-pid=no --disable-syslog --log-timestamp=no --config-dir=/run/powerdns --socket-dir=/run/powerdns diff --git a/src/helpers/vyos-boot-config-loader.py b/src/helpers/vyos-boot-config-loader.py index 01b06526d..42de696ce 100755 --- a/src/helpers/vyos-boot-config-loader.py +++ b/src/helpers/vyos-boot-config-loader.py @@ -102,7 +102,8 @@ def failsafe(config_file_name): 'authentication', 'encrypted-password']) - cmd(f"useradd -s /bin/bash -G 'users,sudo' -m -N -p '{passwd}' vyos") + cmd(f"useradd --create-home --no-user-group --shell /bin/vbash --password '{passwd}' "\ + "--groups frr,frrvty,vyattacfg,sudo,adm,dip,disk vyos") if __name__ == '__main__': if len(sys.argv) < 2: diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh new file mode 100755 index 000000000..d0b663f7b --- /dev/null +++ b/src/helpers/vyos-certbot-renew-pki.sh @@ -0,0 +1,3 @@ +#!/bin/sh +source /opt/vyatta/etc/functions/script-template +/usr/libexec/vyos/conf_mode/pki.py certbot_renew diff --git a/src/migration-scripts/bgp/0-to-1 b/src/migration-scripts/bgp/0-to-1 index 03c45107b..5b8e8a163 100755 --- a/src/migration-scripts/bgp/0-to-1 +++ b/src/migration-scripts/bgp/0-to-1 @@ -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/>. -# T3417: migrate IS-IS tagNode to node as we can only have one IS-IS process +# T3417: migrate BGP tagNode to node as we can only have one BGP process from sys import argv from sys import exit diff --git a/src/migration-scripts/bgp/1-to-2 b/src/migration-scripts/bgp/1-to-2 index 96b939b47..a40d86e67 100755 --- a/src/migration-scripts/bgp/1-to-2 +++ b/src/migration-scripts/bgp/1-to-2 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2022 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,6 +15,7 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. # T3741: no-ipv4-unicast is now enabled by default +# T5937: Migrate IPv6 BGP Neighbor Peer Groups from sys import argv from sys import exit @@ -66,6 +67,15 @@ else: if not config.exists(afi_ipv4): config.set(afi_ipv4) +# Migrate IPv6 AFI peer-group +if config.exists(base + ['neighbor']): + for neighbor in config.list_nodes(base + ['neighbor']): + tmp_path = base + ['neighbor', neighbor, 'address-family', 'ipv6-unicast', 'peer-group'] + if config.exists(tmp_path): + peer_group = config.return_value(tmp_path) + config.set(base + ['neighbor', neighbor, 'peer-group'], value=peer_group) + config.delete(tmp_path) + try: with open(file_name, 'w') as f: f.write(config.to_string()) diff --git a/src/migration-scripts/bgp/4-to-5 b/src/migration-scripts/bgp/4-to-5 new file mode 100755 index 000000000..c4eb9ec72 --- /dev/null +++ b/src/migration-scripts/bgp/4-to-5 @@ -0,0 +1,67 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Delete 'protocols bgp address-family ipv6-unicast route-target vpn +# import/export', if 'protocols bgp address-family ipv6-unicast +# route-target vpn both' exists + +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) + +bgp_base = ['protocols', 'bgp'] +# Delete 'import/export' in default vrf if 'both' exists +if config.exists(bgp_base): + for address_family in ['ipv4-unicast', 'ipv6-unicast']: + rt_path = bgp_base + ['address-family', address_family, 'route-target', + 'vpn'] + if config.exists(rt_path + ['both']): + if config.exists(rt_path + ['import']): + config.delete(rt_path + ['import']) + if config.exists(rt_path + ['export']): + config.delete(rt_path + ['export']) + +# Delete import/export in vrfs if both exists +if config.exists(['vrf', 'name']): + for vrf in config.list_nodes(['vrf', 'name']): + vrf_base = ['vrf', 'name', vrf] + for address_family in ['ipv4-unicast', 'ipv6-unicast']: + rt_path = vrf_base + bgp_base + ['address-family', address_family, + 'route-target', 'vpn'] + if config.exists(rt_path + ['both']): + if config.exists(rt_path + ['import']): + config.delete(rt_path + ['import']) + if config.exists(rt_path + ['export']): + config.delete(rt_path + ['export']) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/dhcp-server/8-to-9 b/src/migration-scripts/dhcp-server/8-to-9 new file mode 100755 index 000000000..810e403a6 --- /dev/null +++ b/src/migration-scripts/dhcp-server/8-to-9 @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T3316: +# - Migrate dhcp options under new option node +# - Add subnet IDs to existing subnets + +import sys +import re +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcp-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +option_nodes = ['bootfile-name', 'bootfile-server', 'bootfile-size', 'captive-portal', + 'client-prefix-length', 'default-router', 'domain-name', 'domain-search', + 'name-server', 'ip-forwarding', 'ipv6-only-preferred', 'ntp-server', + 'pop-server', 'server-identifier', 'smtp-server', 'static-route', + 'tftp-server-name', 'time-offset', 'time-server', 'time-zone', + 'vendor-option', 'wins-server', 'wpad-url'] + +subnet_id = 1 + +for network in config.list_nodes(base): + for option in option_nodes: + if config.exists(base + [network, option]): + config.set(base + [network, 'option']) + config.copy(base + [network, option], base + [network, 'option', option]) + config.delete(base + [network, option]) + + if config.exists(base + [network, 'subnet']): + for subnet in config.list_nodes(base + [network, 'subnet']): + base_subnet = base + [network, 'subnet', subnet] + + for option in option_nodes: + if config.exists(base_subnet + [option]): + config.set(base_subnet + ['option']) + config.copy(base_subnet + [option], base_subnet + ['option', option]) + config.delete(base_subnet + [option]) + + config.set(base_subnet + ['subnet-id'], value=subnet_id) + subnet_id += 1 + +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/dhcpv6-server/3-to-4 b/src/migration-scripts/dhcpv6-server/3-to-4 new file mode 100755 index 000000000..4747ebd60 --- /dev/null +++ b/src/migration-scripts/dhcpv6-server/3-to-4 @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T3316: +# - Add subnet IDs to existing subnets +# - Move options to option node +# - Migrate address-range to range tagNode + +import sys +import re +from vyos.configtree import ConfigTree + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcpv6-server', 'shared-network-name'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + sys.exit(0) + +option_nodes = ['captive-portal', 'domain-search', 'name-server', + 'nis-domain', 'nis-server', 'nisplus-domain', 'nisplus-server', + 'sip-server', 'sntp-server', 'vendor-option'] + +subnet_id = 1 + +for network in config.list_nodes(base): + if config.exists(base + [network, 'subnet']): + for subnet in config.list_nodes(base + [network, 'subnet']): + base_subnet = base + [network, 'subnet', subnet] + + if config.exists(base_subnet + ['address-range']): + config.set(base_subnet + ['range']) + config.set_tag(base_subnet + ['range']) + + range_id = 1 + + if config.exists(base_subnet + ['address-range', 'prefix']): + for prefix in config.return_values(base_subnet + ['address-range', 'prefix']): + config.set(base_subnet + ['range', range_id, 'prefix'], value=prefix) + + range_id += 1 + + if config.exists(base_subnet + ['address-range', 'start']): + for start in config.list_nodes(base_subnet + ['address-range', 'start']): + stop = config.return_value(base_subnet + ['address-range', 'start', start, 'stop']) + + config.set(base_subnet + ['range', range_id, 'start'], value=start) + config.set(base_subnet + ['range', range_id, 'stop'], value=stop) + + range_id += 1 + + config.delete(base_subnet + ['address-range']) + + for option in option_nodes: + if config.exists(base_subnet + [option]): + config.set(base_subnet + ['option']) + config.copy(base_subnet + [option], base_subnet + ['option', option]) + config.delete(base_subnet + [option]) + + config.set(base_subnet + ['subnet-id'], value=subnet_id) + subnet_id += 1 + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print("Failed to save the modified config: {}".format(e)) + exit(1) diff --git a/src/migration-scripts/dns-dynamic/3-to-4 b/src/migration-scripts/dns-dynamic/3-to-4 new file mode 100755 index 000000000..b888a3b6b --- /dev/null +++ b/src/migration-scripts/dns-dynamic/3-to-4 @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5966: +# - migrate "service dns dynamic name <service> address <interface>" +# to "service dns dynamic name <service> address interface <interface>" +# when <interface> != 'web' +# - migrate "service dns dynamic name <service> web-options ..." +# to "service dns dynamic name <service> address web ..." +# when <interface> == 'web' + +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', 'name'] + +if not config.exists(base_path): + # Nothing to do + sys.exit(0) + +for service in config.list_nodes(base_path): + + service_path = base_path + [service] + + if config.exists(service_path + ['address']): + address = config.return_value(service_path + ['address']) + # 'address' is not a leaf node anymore, delete it first + config.delete(service_path + ['address']) + + # When address is an interface (not 'web'), move it to 'address interface' + if address != 'web': + config.set(service_path + ['address', 'interface'], address) + + else: # address == 'web' + # Relocate optional 'web-options' directly under 'address web' + if config.exists(service_path + ['web-options']): + # config.copy does not recursively create a path, so initialize it + config.set(service_path + ['address']) + config.copy(service_path + ['web-options'], + service_path + ['address', 'web']) + config.delete(service_path + ['web-options']) + + # ensure that valueless 'address web' still exists even if there are no 'web-options' + if not config.exists(service_path + ['address', 'web']): + config.set(service_path + ['address', 'web']) + +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/firewall/10-to-11 b/src/migration-scripts/firewall/10-to-11 index e14ea0e51..abb804a28 100755 --- a/src/migration-scripts/firewall/10-to-11 +++ b/src/migration-scripts/firewall/10-to-11 @@ -80,12 +80,27 @@ for option in ['all-ping', 'broadcast-ping', 'config-trap', 'ip-src-route', 'ipv config.delete(base + [option]) ### Migration of firewall name and ipv6-name +### Also migrate legacy 'accept' behaviour if config.exists(base + ['name']): config.set(['firewall', 'ipv4', 'name']) config.set_tag(['firewall', 'ipv4', 'name']) for ipv4name in config.list_nodes(base + ['name']): config.copy(base + ['name', ipv4name], base + ['ipv4', 'name', ipv4name]) + + if config.exists(base + ['ipv4', 'name', ipv4name, 'default-action']): + action = config.return_value(base + ['ipv4', 'name', ipv4name, 'default-action']) + + if action == 'accept': + config.set(base + ['ipv4', 'name', ipv4name, 'default-action'], value='return') + + if config.exists(base + ['ipv4', 'name', ipv4name, 'rule']): + for rule_id in config.list_nodes(base + ['ipv4', 'name', ipv4name, 'rule']): + action = config.return_value(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action']) + + if action == 'accept': + config.set(base + ['ipv4', 'name', ipv4name, 'rule', rule_id, 'action'], value='return') + config.delete(base + ['name']) if config.exists(base + ['ipv6-name']): @@ -94,6 +109,20 @@ if config.exists(base + ['ipv6-name']): for ipv6name in config.list_nodes(base + ['ipv6-name']): config.copy(base + ['ipv6-name', ipv6name], base + ['ipv6', 'name', ipv6name]) + + if config.exists(base + ['ipv6', 'name', ipv6name, 'default-action']): + action = config.return_value(base + ['ipv6', 'name', ipv6name, 'default-action']) + + if action == 'accept': + config.set(base + ['ipv6', 'name', ipv6name, 'default-action'], value='return') + + if config.exists(base + ['ipv6', 'name', ipv6name, 'rule']): + for rule_id in config.list_nodes(base + ['ipv6', 'name', ipv6name, 'rule']): + action = config.return_value(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action']) + + if action == 'accept': + config.set(base + ['ipv6', 'name', ipv6name, 'rule', rule_id, 'action'], value='return') + config.delete(base + ['ipv6-name']) ### Migration of firewall interface @@ -102,8 +131,8 @@ if config.exists(base + ['interface']): inp_ipv4_rule = 5 fwd_ipv6_rule = 5 inp_ipv6_rule = 5 - for iface in config.list_nodes(base + ['interface']): - for direction in ['in', 'out', 'local']: + for direction in ['in', 'out', 'local']: + for iface in config.list_nodes(base + ['interface']): if config.exists(base + ['interface', iface, direction]): if config.exists(base + ['interface', iface, direction, 'name']): target = config.return_value(base + ['interface', iface, direction, 'name']) diff --git a/src/migration-scripts/https/5-to-6 b/src/migration-scripts/https/5-to-6 new file mode 100755 index 000000000..0090adccb --- /dev/null +++ b/src/migration-scripts/https/5-to-6 @@ -0,0 +1,109 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# T5886: Add support for ACME protocol (LetsEncrypt), migrate https certbot +# to new "pki certificate" CLI tree +# T5902: Remove virtual-host + +import os +import sys + +from vyos.configtree import ConfigTree +from vyos.defaults import directories +from vyos.utils.process import cmd + +vyos_certbot_dir = directories['certbot'] + +if len(sys.argv) < 2: + print("Must specify file name!") + sys.exit(1) + +file_name = sys.argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +config = ConfigTree(config_file) + +base = ['service', 'https'] +if not config.exists(base): + # Nothing to do + sys.exit(0) + +if config.exists(base + ['certificates', 'certbot']): + # both domain-name and email must be set on CLI - ensured by previous verify() + domain_names = config.return_values(base + ['certificates', 'certbot', 'domain-name']) + email = config.return_value(base + ['certificates', 'certbot', 'email']) + config.delete(base + ['certificates', 'certbot']) + + # Set default certname based on domain-name + cert_name = 'https-' + domain_names[0].split('.')[0] + # Overwrite certname from previous certbot calls if available + # We can not use python code like os.scandir due to filesystem permissions. + # This must be run as root + certbot_live = f'{vyos_certbot_dir}/live/' # we need the trailing / + if os.path.exists(certbot_live): + tmp = cmd(f'sudo find {certbot_live} -maxdepth 1 -type d') + tmp = tmp.split() # tmp = ['/config/auth/letsencrypt/live', '/config/auth/letsencrypt/live/router.vyos.net'] + tmp.remove(certbot_live) + cert_name = tmp[0].replace(certbot_live, '') + + config.set(['pki', 'certificate', cert_name, 'acme', 'email'], value=email) + config.set_tag(['pki', 'certificate']) + for domain in domain_names: + config.set(['pki', 'certificate', cert_name, 'acme', 'domain-name'], value=domain, replace=False) + + # Update Webserver certificate + config.set(base + ['certificates', 'certificate'], value=cert_name) + +if config.exists(base + ['virtual-host']): + allow_client = [] + listen_port = [] + listen_address = [] + for virtual_host in config.list_nodes(base + ['virtual-host']): + allow_path = base + ['virtual-host', virtual_host, 'allow-client', 'address'] + if config.exists(allow_path): + tmp = config.return_values(allow_path) + allow_client.extend(tmp) + + port_path = base + ['virtual-host', virtual_host, 'listen-port'] + if config.exists(port_path): + tmp = config.return_value(port_path) + listen_port.append(tmp) + + listen_address_path = base + ['virtual-host', virtual_host, 'listen-address'] + if config.exists(listen_address_path): + tmp = config.return_value(listen_address_path) + listen_address.append(tmp) + + config.delete(base + ['virtual-host']) + for client in allow_client: + config.set(base + ['allow-client', 'address'], value=client, replace=False) + + # clear listen-address if "all" were specified + if '*' in listen_address: + listen_address = [] + for address in listen_address: + config.set(base + ['listen-address'], value=address, replace=False) + + + +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 index c8cec6835..11d7911e9 100755 --- a/src/migration-scripts/ipoe-server/1-to-2 +++ b/src/migration-scripts/ipoe-server/1-to-2 @@ -57,7 +57,7 @@ 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) + config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False) # Get netmask from subnet mask = subnet.split("/")[1] if config.exists(pool_path + ['next-pool']): diff --git a/src/migration-scripts/ipoe-server/2-to-3 b/src/migration-scripts/ipoe-server/2-to-3 new file mode 100755 index 000000000..d4ae0a7ba --- /dev/null +++ b/src/migration-scripts/ipoe-server/2-to-3 @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Migrating to named ipv6 pools + +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-ipv6-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +ipv6_pool_name = 'ipv6-pool' +config.copy(pool_base, pool_base + [ipv6_pool_name]) + +if config.exists(pool_base + ['prefix']): + config.delete(pool_base + ['prefix']) + config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) +if config.exists(pool_base + ['delegate']): + config.delete(pool_base + ['delegate']) + +# 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/ipsec/12-to-13 b/src/migration-scripts/ipsec/12-to-13 new file mode 100755 index 000000000..c11f708bd --- /dev/null +++ b/src/migration-scripts/ipsec/12-to-13 @@ -0,0 +1,59 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Changed value of dead-peer-detection.action from hold to trap +# Changed value of close-action from hold to trap and from restart to start + +import re + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['vpn', 'ipsec', 'ike-group'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) +else: + for ike_group in config.list_nodes(base): + base_dpd_action = base + [ike_group, 'dead-peer-detection', 'action'] + base_close_action = base + [ike_group, 'close-action'] + if config.exists(base_dpd_action) and config.return_value(base_dpd_action) == 'hold': + config.set(base_dpd_action, 'trap', replace=True) + if config.exists(base_close_action): + if config.return_value(base_close_action) == 'hold': + config.set(base_close_action, 'trap', replace=True) + if config.return_value(base_close_action) == 'restart': + config.set(base_close_action, 'start', replace=True) + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1) diff --git a/src/migration-scripts/l2tp/4-to-5 b/src/migration-scripts/l2tp/4-to-5 index 496dc83d6..3176f895a 100755 --- a/src/migration-scripts/l2tp/4-to-5 +++ b/src/migration-scripts/l2tp/4-to-5 @@ -24,7 +24,7 @@ import os from sys import argv from sys import exit from vyos.configtree import ConfigTree - +from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") @@ -45,33 +45,33 @@ if not config.exists(pool_base): exit(0) default_pool = '' range_pool_name = 'default-range-pool' -subnet_base_name = 'default-subnet-pool' -number = 1 -subnet_pool_name = f'{subnet_base_name}-{number}' -prev_subnet_pool = subnet_pool_name -if config.exists(pool_base + ['subnet']): - default_pool = subnet_pool_name - for subnet in config.return_values(pool_base + ['subnet']): - config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) - if prev_subnet_pool != subnet_pool_name: - config.set(pool_base + [prev_subnet_pool, 'next-pool'], - value=subnet_pool_name) - prev_subnet_pool = subnet_pool_name - number += 1 - subnet_pool_name = f'{subnet_base_name}-{number}' - - config.delete(pool_base + ['subnet']) if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + def is_legalrange(ip1: str, ip2: str, mask: str): + from ipaddress import IPv4Interface + interface1 = IPv4Interface(f'{ip1}/{mask}') + + interface2 = IPv4Interface(f'{ip2}/{mask}') + return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip + start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) - ip_range = f'{start_ip}-{stop_ip}' + if is_legalrange(start_ip, stop_ip,'24'): + ip_range = f'{start_ip}-{stop_ip}' + config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) + default_pool = range_pool_name + else: + Warning( + f'L2TP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') + 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=default_pool) + +if config.exists(pool_base + ['subnet']): + for subnet in config.return_values(pool_base + ['subnet']): + config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) + + config.delete(pool_base + ['subnet']) default_pool = range_pool_name if default_pool: diff --git a/src/migration-scripts/l2tp/6-to-7 b/src/migration-scripts/l2tp/6-to-7 new file mode 100755 index 000000000..f49c4ab08 --- /dev/null +++ b/src/migration-scripts/l2tp/6-to-7 @@ -0,0 +1,60 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Migrating to named ipv6 pools + +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-ipv6-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +ipv6_pool_name = 'ipv6-pool' +config.copy(pool_base, pool_base + [ipv6_pool_name]) + +if config.exists(pool_base + ['prefix']): + config.delete(pool_base + ['prefix']) + config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) +if config.exists(pool_base + ['delegate']): + config.delete(pool_base + ['delegate']) +# 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/nat/5-to-6 b/src/migration-scripts/nat/5-to-6 index de3830582..c83b93d84 100755 --- a/src/migration-scripts/nat/5-to-6 +++ b/src/migration-scripts/nat/5-to-6 @@ -51,8 +51,9 @@ for direction in ['source', 'destination']: for iface in ['inbound-interface','outbound-interface']: if config.exists(base + [iface]): tmp = config.return_value(base + [iface]) - config.delete(base + [iface]) - config.set(base + [iface, 'interface-name'], value=tmp) + if tmp: + config.delete(base + [iface]) + config.set(base + [iface, 'interface-name'], value=tmp) try: with open(file_name, 'w') as f: diff --git a/src/migration-scripts/ospf/0-to-1 b/src/migration-scripts/ospf/0-to-1 index 8f02acada..a6cb9feb8 100755 --- a/src/migration-scripts/ospf/0-to-1 +++ b/src/migration-scripts/ospf/0-to-1 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -28,6 +28,7 @@ def ospf_passive_migration(config, ospf_base): default = True continue config.set(ospf_base + ['interface', interface, 'passive']) + config.set_tag(ospf_base + ['interface']) config.delete(ospf_base + ['passive-interface']) config.set(ospf_base + ['passive-interface'], value='default') @@ -35,6 +36,7 @@ def ospf_passive_migration(config, ospf_base): if config.exists(ospf_base + ['passive-interface-exclude']): for interface in config.return_values(ospf_base + ['passive-interface-exclude']): config.set(ospf_base + ['interface', interface, 'passive', 'disable']) + config.set_tag(ospf_base + ['interface']) config.delete(ospf_base + ['passive-interface-exclude']) if len(argv) < 2: diff --git a/src/migration-scripts/policy/4-to-5 b/src/migration-scripts/policy/4-to-5 index f6f889c35..5b8fee17e 100755 --- a/src/migration-scripts/policy/4-to-5 +++ b/src/migration-scripts/policy/4-to-5 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -37,7 +37,53 @@ base4 = ['policy', 'route'] base6 = ['policy', 'route6'] config = ConfigTree(config_file) + +def delete_orphaned_interface_policy(config, iftype, ifname, vif=None, vifs=None, vifc=None): + """Delete unexpected policy on interfaces in cases when + policy does not exist but inreface has a policy configuration + Example T5941: + set interfaces bonding bond0 vif 995 policy + """ + if_path = ['interfaces', iftype, ifname] + + if vif: + if_path += ['vif', vif] + elif vifs: + if_path += ['vif-s', vifs] + if vifc: + if_path += ['vif-c', vifc] + + if not config.exists(if_path + ['policy']): + return + + config.delete(if_path + ['policy']) + + if not config.exists(base4) and not config.exists(base6): + # Delete orphaned nodes on interfaces T5941 + for iftype in config.list_nodes(['interfaces']): + for ifname in config.list_nodes(['interfaces', iftype]): + delete_orphaned_interface_policy(config, iftype, ifname) + + if config.exists(['interfaces', iftype, ifname, 'vif']): + for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): + delete_orphaned_interface_policy(config, iftype, ifname, vif=vif) + + if config.exists(['interfaces', iftype, ifname, 'vif-s']): + for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): + delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs) + + if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs, vifc=vifc) + + 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) + # Nothing to do exit(0) diff --git a/src/migration-scripts/pppoe-server/6-to-7 b/src/migration-scripts/pppoe-server/6-to-7 index d856c1f34..b94ce57f9 100755 --- a/src/migration-scripts/pppoe-server/6-to-7 +++ b/src/migration-scripts/pppoe-server/6-to-7 @@ -29,7 +29,7 @@ import os from sys import argv from sys import exit from vyos.configtree import ConfigTree - +from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") @@ -48,38 +48,35 @@ if not config.exists(base): if not config.exists(pool_base): exit(0) + default_pool = '' range_pool_name = 'default-range-pool' -subnet_base_name = 'default-subnet-pool' -number = 1 -subnet_pool_name = f'{subnet_base_name}-{number}' -prev_subnet_pool = subnet_pool_name #Default nameless pools migrations -if config.exists(pool_base + ['subnet']): - default_pool = subnet_pool_name - for subnet in config.return_values(pool_base + ['subnet']): - config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) - if prev_subnet_pool != subnet_pool_name: - config.set(pool_base + [prev_subnet_pool, 'next-pool'], - value=subnet_pool_name) - prev_subnet_pool = subnet_pool_name - number += 1 - subnet_pool_name = f'{subnet_base_name}-{number}' - - config.delete(pool_base + ['subnet']) - if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + def is_legalrange(ip1: str, ip2: str, mask: str): + from ipaddress import IPv4Interface + interface1 = IPv4Interface(f'{ip1}/{mask}') + interface2 = IPv4Interface(f'{ip2}/{mask}') + return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip + start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) - ip_range = f'{start_ip}-{stop_ip}' + if is_legalrange(start_ip, stop_ip, '24'): + ip_range = f'{start_ip}-{stop_ip}' + config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) + default_pool = range_pool_name + else: + Warning( + f'PPPoE client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') 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=default_pool) + +if config.exists(pool_base + ['subnet']): default_pool = range_pool_name + for subnet in config.return_values(pool_base + ['subnet']): + config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) + config.delete(pool_base + ['subnet']) gateway = '' if config.exists(base + ['gateway-address']): @@ -97,7 +94,7 @@ if config.exists(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) + config.set(pool_base + [pool_name, 'range'], value=subnet, replace=False) 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) diff --git a/src/migration-scripts/pppoe-server/7-to-8 b/src/migration-scripts/pppoe-server/7-to-8 new file mode 100755 index 000000000..b0d9bb464 --- /dev/null +++ b/src/migration-scripts/pppoe-server/7-to-8 @@ -0,0 +1,61 @@ +#!/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/>. + +# Migrating to named ipv6 pools + +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-ipv6-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +ipv6_pool_name = 'ipv6-pool' +config.copy(pool_base, pool_base + [ipv6_pool_name]) + +if config.exists(pool_base + ['prefix']): + config.delete(pool_base + ['prefix']) + config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) +if config.exists(pool_base + ['delegate']): + config.delete(pool_base + ['delegate']) + +# 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 index 98dc5c2a6..091cb68ec 100755 --- a/src/migration-scripts/pptp/2-to-3 +++ b/src/migration-scripts/pptp/2-to-3 @@ -23,7 +23,7 @@ import os from sys import argv from sys import exit from vyos.configtree import ConfigTree - +from vyos.base import Warning if len(argv) < 2: print("Must specify file name!") @@ -46,13 +46,24 @@ if not config.exists(pool_base): range_pool_name = 'default-range-pool' if config.exists(pool_base + ['start']) and config.exists(pool_base + ['stop']): + def is_legalrange(ip1: str, ip2: str, mask: str): + from ipaddress import IPv4Interface + interface1 = IPv4Interface(f'{ip1}/{mask}') + interface2 = IPv4Interface(f'{ip2}/{mask}') + return interface1.network.network_address == interface2.network.network_address and interface2.ip > interface1.ip + start_ip = config.return_value(pool_base + ['start']) stop_ip = config.return_value(pool_base + ['stop']) - ip_range = f'{start_ip}-{stop_ip}' + if is_legalrange(start_ip, stop_ip, '24'): + ip_range = f'{start_ip}-{stop_ip}' + config.set(pool_base + [range_pool_name, 'range'], value=ip_range, replace=False) + config.set(base + ['default-pool'], value=range_pool_name) + else: + Warning( + f'PPTP client-ip-pool range start-ip:{start_ip} and stop-ip:{stop_ip} can not be migrated.') + 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) diff --git a/src/migration-scripts/qos/1-to-2 b/src/migration-scripts/qos/1-to-2 index cca32d06e..666811e5a 100755 --- a/src/migration-scripts/qos/1-to-2 +++ b/src/migration-scripts/qos/1-to-2 @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -40,7 +40,53 @@ with open(file_name, 'r') as f: base = ['traffic-policy'] config = ConfigTree(config_file) + +def delete_orphaned_interface_policy(config, iftype, ifname, vif=None, vifs=None, vifc=None): + """Delete unexpected traffic-policy on interfaces in cases when + policy does not exist but inreface has a policy configuration + Example T5941: + set interfaces bonding bond0 vif 995 traffic-policy + """ + if_path = ['interfaces', iftype, ifname] + + if vif: + if_path += ['vif', vif] + elif vifs: + if_path += ['vif-s', vifs] + if vifc: + if_path += ['vif-c', vifc] + + if not config.exists(if_path + ['traffic-policy']): + return + + config.delete(if_path + ['traffic-policy']) + + if not config.exists(base): + # Delete orphaned nodes on interfaces T5941 + for iftype in config.list_nodes(['interfaces']): + for ifname in config.list_nodes(['interfaces', iftype]): + delete_orphaned_interface_policy(config, iftype, ifname) + + if config.exists(['interfaces', iftype, ifname, 'vif']): + for vif in config.list_nodes(['interfaces', iftype, ifname, 'vif']): + delete_orphaned_interface_policy(config, iftype, ifname, vif=vif) + + if config.exists(['interfaces', iftype, ifname, 'vif-s']): + for vifs in config.list_nodes(['interfaces', iftype, ifname, 'vif-s']): + delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs) + + if config.exists(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + for vifc in config.list_nodes(['interfaces', iftype, ifname, 'vif-s', vifs, 'vif-c']): + delete_orphaned_interface_policy(config, iftype, ifname, vifs=vifs, vifc=vifc) + + 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) + # Nothing to do exit(0) diff --git a/src/migration-scripts/sstp/4-to-5 b/src/migration-scripts/sstp/4-to-5 index 3a86c79ec..95e482713 100755 --- a/src/migration-scripts/sstp/4-to-5 +++ b/src/migration-scripts/sstp/4-to-5 @@ -43,21 +43,12 @@ if not config.exists(base): if not config.exists(pool_base): exit(0) -subnet_base_name = 'default-subnet-pool' -number = 1 -subnet_pool_name = f'{subnet_base_name}-{number}' -prev_subnet_pool = subnet_pool_name +range_pool_name = 'default-range-pool' + if config.exists(pool_base + ['subnet']): - default_pool = subnet_pool_name + default_pool = range_pool_name for subnet in config.return_values(pool_base + ['subnet']): - config.set(pool_base + [subnet_pool_name, 'range'], value=subnet) - if prev_subnet_pool != subnet_pool_name: - config.set(pool_base + [prev_subnet_pool, 'next-pool'], - value=subnet_pool_name) - prev_subnet_pool = subnet_pool_name - number += 1 - subnet_pool_name = f'{subnet_base_name}-{number}' - + config.set(pool_base + [range_pool_name, 'range'], value=subnet, replace=False) config.delete(pool_base + ['subnet']) config.set(base + ['default-pool'], value=default_pool) # format as tag node diff --git a/src/migration-scripts/sstp/5-to-6 b/src/migration-scripts/sstp/5-to-6 new file mode 100755 index 000000000..bac9975b2 --- /dev/null +++ b/src/migration-scripts/sstp/5-to-6 @@ -0,0 +1,62 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# Migrating to named ipv6 pools + +import os +import pprint + +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-ipv6-pool'] +if not config.exists(base): + exit(0) + +if not config.exists(pool_base): + exit(0) + +ipv6_pool_name = 'ipv6-pool' +config.copy(pool_base, pool_base + [ipv6_pool_name]) + +if config.exists(pool_base + ['prefix']): + config.delete(pool_base + ['prefix']) + config.set(base + ['default-ipv6-pool'], value=ipv6_pool_name) +if config.exists(pool_base + ['delegate']): + config.delete(pool_base + ['delegate']) + +# 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/dhcp.py b/src/op_mode/dhcp.py index 02f4d5bbb..a64acec31 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -194,14 +194,11 @@ def _get_pool_size(pool, family='inet'): size = 0 subnets = config.list_nodes(f'{base} subnet') for subnet in subnets: - if family == 'inet6': - ranges = config.list_nodes(f'{base} subnet {subnet} address-range start') - else: - ranges = config.list_nodes(f'{base} subnet {subnet} range') + ranges = config.list_nodes(f'{base} subnet {subnet} range') for range in ranges: if family == 'inet6': - start = config.list_nodes(f'{base} subnet {subnet} address-range start')[0] - stop = config.value(f'{base} subnet {subnet} address-range start {start} stop') + start = config.value(f'{base} subnet {subnet} range {range} start') + stop = config.value(f'{base} subnet {subnet} range {range} stop') else: start = config.value(f'{base} subnet {subnet} range {range} start') stop = config.value(f'{base} subnet {subnet} range {range} stop') diff --git a/src/op_mode/dns.py b/src/op_mode/dns.py index 2168aef89..16c462f23 100755 --- a/src/op_mode/dns.py +++ b/src/op_mode/dns.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -15,17 +15,35 @@ # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import sys +import time +import typing +import vyos.opmode from tabulate import tabulate - from vyos.configquery import ConfigTreeQuery -from vyos.utils.process import cmd - -import vyos.opmode - - -def _data_to_dict(data, sep="\t") -> dict: +from vyos.utils.process import cmd, rc_cmd +from vyos.template import is_ipv4, is_ipv6 + +_dynamic_cache_file = r'/run/ddclient/ddclient.cache' + +_dynamic_status_columns = { + 'host': 'Hostname', + 'ipv4': 'IPv4 address', + 'status-ipv4': 'IPv4 status', + 'ipv6': 'IPv6 address', + 'status-ipv6': 'IPv6 status', + 'mtime': 'Last update', +} + +_forwarding_statistics_columns = { + 'cache-entries': 'Cache entries', + 'max-cache-entries': 'Max cache entries', + 'cache-size': 'Cache size', +} + +def _forwarding_data_to_dict(data, sep="\t") -> dict: """ Return dictionary from plain text separated by tab @@ -51,37 +69,135 @@ def _data_to_dict(data, sep="\t") -> dict: dictionary[key] = value return dictionary +def _get_dynamic_host_records_raw() -> dict: + + data = [] + + if os.path.isfile(_dynamic_cache_file): # A ddclient status file might not always exist + with open(_dynamic_cache_file, 'r') as f: + for line in f: + if line.startswith('#'): + continue + + props = {} + # ddclient cache rows have properties in 'key=value' format separated by comma + # we pick up the ones we are interested in + for kvraw in line.split(' ')[0].split(','): + k, v = kvraw.split('=') + if k in list(_dynamic_status_columns.keys()) + ['ip', 'status']: # ip and status are legacy keys + props[k] = v + + # Extract IPv4 and IPv6 address and status from legacy keys + # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 + if 'ip' in props: + if is_ipv4(props['ip']): + props['ipv4'] = props['ip'] + props['status-ipv4'] = props['status'] + elif is_ipv6(props['ip']): + props['ipv6'] = props['ip'] + props['status-ipv6'] = props['status'] + del props['ip'] + + # Convert mtime to human readable format + if 'mtime' in props: + props['mtime'] = time.strftime( + "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) + + data.append(props) -def _get_raw_forwarding_statistics() -> dict: - command = cmd('rec_control --socket-dir=/run/powerdns get-all') - data = _data_to_dict(command) - data['cache-size'] = "{0:.2f}".format( int( - cmd('rec_control --socket-dir=/run/powerdns get cache-bytes')) / 1024 ) return data - -def _get_formatted_forwarding_statistics(data): - cache_entries = data.get('cache-entries') - max_cache_entries = data.get('max-cache-entries') - cache_size = data.get('cache-size') - data_entries = [[cache_entries, max_cache_entries, f'{cache_size} kbytes']] - headers = ["Cache entries", "Max cache entries" , "Cache size"] - output = tabulate(data_entries, headers, numalign="left") +def _get_dynamic_host_records_formatted(data): + data_entries = [] + for entry in data: + data_entries.append([entry.get(key) for key in _dynamic_status_columns.keys()]) + header = _dynamic_status_columns.values() + output = tabulate(data_entries, header, numalign='left') return output +def _get_forwarding_statistics_raw() -> dict: + command = cmd('rec_control get-all') + data = _forwarding_data_to_dict(command) + data['cache-size'] = "{0:.2f} kbytes".format( int( + cmd('rec_control get cache-bytes')) / 1024 ) + return data -def show_forwarding_statistics(raw: bool): +def _get_forwarding_statistics_formatted(data): + data_entries = [] + data_entries.append([data.get(key) for key in _forwarding_statistics_columns.keys()]) + header = _forwarding_statistics_columns.values() + output = tabulate(data_entries, header, numalign='left') + return output - config = ConfigTreeQuery() - if not config.exists('service dns forwarding'): - raise vyos.opmode.UnconfiguredSubsystem('DNS forwarding is not configured') +def _verify(target): + """Decorator checks if config for DNS related service exists""" + from functools import wraps + + if target not in ['dynamic', 'forwarding']: + raise ValueError('Invalid target') + + def _verify_target(func): + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + if not config.exists(f'service dns {target}'): + _prefix = f'Dynamic DNS' if target == 'dynamic' else 'DNS Forwarding' + raise vyos.opmode.UnconfiguredSubsystem(f'{_prefix} is not configured') + return func(*args, **kwargs) + return _wrapper + return _verify_target + +@_verify('dynamic') +def show_dynamic_status(raw: bool): + host_data = _get_dynamic_host_records_raw() + if raw: + return host_data + else: + return _get_dynamic_host_records_formatted(host_data) - dns_data = _get_raw_forwarding_statistics() +@_verify('dynamic') +def reset_dynamic(): + """ + Reset Dynamic DNS cache + """ + if os.path.exists(_dynamic_cache_file): + os.remove(_dynamic_cache_file) + rc, output = rc_cmd('systemctl restart ddclient.service') + if rc != 0: + print(output) + return None + print(f'Dynamic DNS state reset!') + +@_verify('forwarding') +def show_forwarding_statistics(raw: bool): + dns_data = _get_forwarding_statistics_raw() if raw: return dns_data else: - return _get_formatted_forwarding_statistics(dns_data) + return _get_forwarding_statistics_formatted(dns_data) + +@_verify('forwarding') +def reset_forwarding(all: bool, domain: typing.Optional[str]): + """ + Reset DNS Forwarding cache + :param all (bool): reset cache all domains + :param domain (str): reset cache for specified domain + """ + if all: + rc, output = rc_cmd('rec_control wipe-cache ".$"') + if rc != 0: + print(output) + return None + print('DNS Forwarding cache reset for all domains!') + return output + elif domain: + rc, output = rc_cmd(f'rec_control wipe-cache "{domain}$"') + if rc != 0: + print(output) + return None + print(f'DNS Forwarding cache reset for domain "{domain}"!') + return output if __name__ == '__main__': try: diff --git a/src/op_mode/dns_dynamic.py b/src/op_mode/dns_dynamic.py deleted file mode 100755 index 12aa5494a..000000000 --- a/src/op_mode/dns_dynamic.py +++ /dev/null @@ -1,113 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018-2023 VyOS maintainers and contributors -# -# This program is free software; you can redistribute it and/or modify -# it under the terms of the GNU General Public License version 2 or later as -# published by the Free Software Foundation. -# -# This program is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see <http://www.gnu.org/licenses/>. - -import os -import argparse -import sys -import time -from tabulate import tabulate - -from vyos.config import Config -from vyos.template import is_ipv4, is_ipv6 -from vyos.utils.process import call - -cache_file = r'/run/ddclient/ddclient.cache' - -columns = { - 'host': 'Hostname', - 'ipv4': 'IPv4 address', - 'status-ipv4': 'IPv4 status', - 'ipv6': 'IPv6 address', - 'status-ipv6': 'IPv6 status', - 'mtime': 'Last update', -} - - -def _get_formatted_host_records(host_data): - data_entries = [] - for entry in host_data: - data_entries.append([entry.get(key) for key in columns.keys()]) - - header = columns.values() - output = tabulate(data_entries, header, numalign='left') - return output - - -def show_status(): - # A ddclient status file might not always exist - if not os.path.exists(cache_file): - sys.exit(0) - - data = [] - - with open(cache_file, 'r') as f: - for line in f: - if line.startswith('#'): - continue - - props = {} - # ddclient cache rows have properties in 'key=value' format separated by comma - # we pick up the ones we are interested in - for kvraw in line.split(' ')[0].split(','): - k, v = kvraw.split('=') - if k in list(columns.keys()) + ['ip', 'status']: # ip and status are legacy keys - props[k] = v - - # Extract IPv4 and IPv6 address and status from legacy keys - # Dual-stack isn't supported in legacy format, 'ip' and 'status' are for one of IPv4 or IPv6 - if 'ip' in props: - if is_ipv4(props['ip']): - props['ipv4'] = props['ip'] - props['status-ipv4'] = props['status'] - elif is_ipv6(props['ip']): - props['ipv6'] = props['ip'] - props['status-ipv6'] = props['status'] - del props['ip'] - - # Convert mtime to human readable format - if 'mtime' in props: - props['mtime'] = time.strftime( - "%Y-%m-%d %H:%M:%S", time.localtime(int(props['mtime'], base=10))) - - data.append(props) - - print(_get_formatted_host_records(data)) - - -def update_ddns(): - call('systemctl stop ddclient.service') - if os.path.exists(cache_file): - os.remove(cache_file) - call('systemctl start ddclient.service') - - -if __name__ == '__main__': - parser = argparse.ArgumentParser() - group = parser.add_mutually_exclusive_group() - group.add_argument("--status", help="Show DDNS status", action="store_true") - group.add_argument("--update", help="Update DDNS on a given interface", action="store_true") - args = parser.parse_args() - - # Do nothing if service is not configured - c = Config() - if not c.exists_effective('service dns dynamic'): - print("Dynamic DNS not configured") - sys.exit(1) - - if args.status: - show_status() - elif args.update: - update_ddns() diff --git a/src/op_mode/dns_forwarding_reset.py b/src/op_mode/dns_forwarding_reset.py deleted file mode 100755 index 55e20918f..000000000 --- a/src/op_mode/dns_forwarding_reset.py +++ /dev/null @@ -1,54 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright (C) 2018 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/>. -# -# File: vyos-show-version -# Purpose: -# Displays image version and system information. -# Used by the "run show version" command. - - -import os -import argparse - -from sys import exit -from vyos.config import Config -from vyos.utils.process import call - -PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' - -parser = argparse.ArgumentParser() -parser.add_argument("-a", "--all", action="store_true", help="Reset all cache") -parser.add_argument("domain", type=str, nargs="?", help="Domain to reset cache entries for") - -if __name__ == '__main__': - args = parser.parse_args() - - # Do nothing if service is not configured - c = Config() - if not c.exists_effective(['service', 'dns', 'forwarding']): - print("DNS forwarding is not configured") - exit(0) - - if args.all: - call(f"{PDNS_CMD} wipe-cache \'.$\'") - exit(0) - - elif args.domain: - call(f"{PDNS_CMD} wipe-cache \'{0}$\'".format(args.domain)) - - else: - parser.print_help() - exit(1) diff --git a/src/op_mode/dns_forwarding_restart.sh b/src/op_mode/dns_forwarding_restart.sh deleted file mode 100755 index 64cc92115..000000000 --- a/src/op_mode/dns_forwarding_restart.sh +++ /dev/null @@ -1,8 +0,0 @@ -#!/bin/sh - -if cli-shell-api existsEffective service dns forwarding; then - echo "Restarting the DNS forwarding service" - systemctl restart pdns-recursor.service -else - echo "DNS forwarding is not configured" -fi diff --git a/src/op_mode/dns_forwarding_statistics.py b/src/op_mode/dns_forwarding_statistics.py deleted file mode 100755 index 32b5c76a7..000000000 --- a/src/op_mode/dns_forwarding_statistics.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 - -import jinja2 -from sys import exit - -from vyos.config import Config -from vyos.utils.process import cmd - -PDNS_CMD='/usr/bin/rec_control --socket-dir=/run/powerdns' - -OUT_TMPL_SRC = """ -DNS forwarding statistics: - -Cache entries: {{ cache_entries }} -Cache size: {{ cache_size }} kbytes - -""" - -if __name__ == '__main__': - # Do nothing if service is not configured - c = Config() - if not c.exists_effective('service dns forwarding'): - print("DNS forwarding is not configured") - exit(0) - - data = {} - - data['cache_entries'] = cmd(f'{PDNS_CMD} get cache-entries') - data['cache_size'] = "{0:.2f}".format( int(cmd(f'{PDNS_CMD} get cache-bytes')) / 1024 ) - - tmpl = jinja2.Template(OUT_TMPL_SRC) - print(tmpl.render(data)) diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 791d76718..501e9b804 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -69,8 +69,8 @@ MSG_WARN_ISO_SIGN_INVALID: str = 'Signature is not valid. Do you want to continu MSG_WARN_ISO_SIGN_UNAVAL: str = 'Signature is not available. Do you want to continue with installation?' MSG_WARN_ROOT_SIZE_TOOBIG: str = 'The size is too big. Try again.' MSG_WARN_ROOT_SIZE_TOOSMALL: str = 'The size is too small. Try again' -MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n' -'It must be between 1 and 32 characters long and contains only the next characters: .+-_ a-z A-Z 0-9' +MSG_WARN_IMAGE_NAME_WRONG: str = 'The suggested name is unsupported!\n'\ +'It must be between 1 and 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9' CONST_MIN_DISK_SIZE: int = 2147483648 # 2 GB CONST_MIN_ROOT_SIZE: int = 1610612736 # 1.5 GB # a reserved space: 2MB for header, 1 MB for BIOS partition, 256 MB for EFI @@ -93,6 +93,7 @@ DEFAULT_BOOT_VARS: dict[str, str] = { 'timeout': '5', 'console_type': 'tty', 'console_num': '0', + 'console_speed': '115200', 'bootmode': 'normal' } @@ -811,7 +812,11 @@ def add_image(image_path: str, vrf: str = None, username: str = '', f'Adding image would downgrade image tools to v.{cfg_ver}; disallowed') if not no_prompt: - image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) + while True: + image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name) + if image.validate_name(image_name): + break + print(MSG_WARN_IMAGE_NAME_WRONG) set_as_default: bool = ask_yes_no(MSG_INPUT_IMAGE_DEFAULT, default=True) else: image_name: str = version_name @@ -866,7 +871,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '', except Exception as err: # unmount an ISO and cleanup cleanup([str(iso_path)]) - exit(f'Whooops: {err}') + exit(f'Error: {err}') def parse_arguments() -> Namespace: diff --git a/src/op_mode/image_manager.py b/src/op_mode/image_manager.py index e75485f9f..e64a85b95 100755 --- a/src/op_mode/image_manager.py +++ b/src/op_mode/image_manager.py @@ -33,6 +33,27 @@ DELETE_IMAGE_PROMPT_MSG: str = 'Select an image to delete:' MSG_DELETE_IMAGE_RUNNING: str = 'Currently running image cannot be deleted; reboot into another image first' MSG_DELETE_IMAGE_DEFAULT: str = 'Default image cannot be deleted; set another image as default first' +def annotated_list(images_list: list[str]) -> list[str]: + """Annotate list of images with additional info + + Args: + images_list (list[str]): a list of image names + + Returns: + list[str]: a list of image names with additional info + """ + index_running: int = None + index_default: int = None + try: + index_running = images_list.index(image.get_running_image()) + index_default = images_list.index(image.get_default_image()) + except ValueError: + pass + if index_running is not None: + images_list[index_running] += ' (running)' + if index_default is not None: + images_list[index_default] += ' (default boot)' + return images_list @compat.grub_cfg_update def delete_image(image_name: Optional[str] = None, @@ -42,7 +63,7 @@ def delete_image(image_name: Optional[str] = None, Args: image_name (str): a name of image to delete """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if no_prompt: exit('An image name is required for delete action') @@ -83,7 +104,7 @@ def set_image(image_name: Optional[str] = None, Args: image_name (str): an image name """ - available_images: list[str] = grub.version_list() + available_images: list[str] = annotated_list(grub.version_list()) if image_name is None: if not prompt: exit('An image name is required for set action') diff --git a/src/op_mode/interfaces_wireless.py b/src/op_mode/interfaces_wireless.py index dfe50e2cb..259fd3900 100755 --- a/src/op_mode/interfaces_wireless.py +++ b/src/op_mode/interfaces_wireless.py @@ -32,6 +32,7 @@ def _verify(func): def _wrapper(*args, **kwargs): config = ConfigTreeQuery() if not config.exists(['interfaces', 'wireless']): + unconf_message = 'No Wireless interfaces configured' raise vyos.opmode.UnconfiguredSubsystem(unconf_message) return func(*args, **kwargs) return _wrapper diff --git a/src/op_mode/multicast.py b/src/op_mode/multicast.py new file mode 100755 index 000000000..0666f8af3 --- /dev/null +++ b/src/op_mode/multicast.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json +import sys +import typing + +from tabulate import tabulate +from vyos.utils.process import cmd + +import vyos.opmode + +ArgFamily = typing.Literal['inet', 'inet6'] + +def _get_raw_data(family, interface=None): + tmp = 'ip -4' + if family == 'inet6': + tmp = 'ip -6' + tmp = f'{tmp} -j maddr show' + if interface: + tmp = f'{tmp} dev {interface}' + output = cmd(tmp) + data = json.loads(output) + if not data: + return [] + return data + +def _get_formatted_output(raw_data): + data_entries = [] + + # sort result by interface name + for interface in sorted(raw_data, key=lambda x: x['ifname']): + for address in interface['maddr']: + tmp = [] + tmp.append(interface['ifname']) + tmp.append(address['family']) + tmp.append(address['address']) + + data_entries.append(tmp) + + headers = ["Interface", "Family", "Address"] + output = tabulate(data_entries, headers, numalign="left") + return output + +def show_group(raw: bool, family: ArgFamily, interface: typing.Optional[str]): + multicast_data = _get_raw_data(family=family, interface=interface) + if raw: + return multicast_data + else: + return _get_formatted_output(multicast_data) + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/pki.py b/src/op_mode/pki.py index 6c854afb5..ad2c1ada0 100755 --- a/src/op_mode/pki.py +++ b/src/op_mode/pki.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -25,6 +25,7 @@ from cryptography import x509 from cryptography.x509.oid import ExtendedKeyUsageOID from vyos.config import Config +from vyos.config import config_dict_mangle_acme from vyos.pki import encode_certificate, encode_public_key, encode_private_key, encode_dh_parameters from vyos.pki import get_certificate_fingerprint from vyos.pki import create_certificate, create_certificate_request, create_certificate_revocation_list @@ -79,9 +80,14 @@ def get_config_certificate(name=None): if not conf.exists(base + ['private', 'key']) or not conf.exists(base + ['certificate']): return False - return conf.get_config_dict(base, key_mangling=('-', '_'), + pki = conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) + if pki: + for certificate in pki: + pki[certificate] = config_dict_mangle_acme(certificate, pki[certificate]) + + return pki def get_certificate_ca(cert, ca_certs): # Find CA certificate for given certificate @@ -1073,7 +1079,9 @@ if __name__ == '__main__': show_crl(None if args.crl == 'all' else args.crl, args.pem) else: show_certificate_authority() + print('\n') show_certificate() + print('\n') show_crl() except KeyboardInterrupt: print("Aborted") diff --git a/src/op_mode/powerctrl.py b/src/op_mode/powerctrl.py index 3ac5991b4..c07d0c4bd 100755 --- a/src/op_mode/powerctrl.py +++ b/src/op_mode/powerctrl.py @@ -191,7 +191,7 @@ def main(): nargs="*", metavar="HH:MM") - action.add_argument("--reboot_in", "-i", + action.add_argument("--reboot-in", "-i", help="Reboot the system", nargs="*", metavar="Minutes") @@ -214,7 +214,7 @@ def main(): if args.reboot is not None: for r in args.reboot: if ':' not in r and '/' not in r and '.' not in r: - print("Incorrect format! Use HH:MM") + print("Incorrect format! Use HH:MM") exit(1) execute_shutdown(args.reboot, reboot=True, ask=args.yes) if args.reboot_in is not None: diff --git a/src/op_mode/show_openvpn.py b/src/op_mode/show_openvpn.py index e29e594a5..6abafc8b6 100755 --- a/src/op_mode/show_openvpn.py +++ b/src/op_mode/show_openvpn.py @@ -63,9 +63,11 @@ def get_vpn_tunnel_address(peer, interface): # filter out subnet entries lst = [l for l in lst[1:] if '/' not in l.split(',')[0]] - tunnel_ip = lst[0].split(',')[0] + if lst: + tunnel_ip = lst[0].split(',')[0] + return tunnel_ip - return tunnel_ip + return 'n/a' def get_status(mode, interface): status_file = '/var/run/openvpn/{}.status'.format(interface) diff --git a/src/op_mode/zone.py b/src/op_mode/zone.py new file mode 100644 index 000000000..d24b1065b --- /dev/null +++ b/src/op_mode/zone.py @@ -0,0 +1,215 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. +import typing +import sys +import vyos.opmode + +import tabulate +from vyos.configquery import ConfigTreeQuery +from vyos.utils.dict import dict_search_args +from vyos.utils.dict import dict_search + + +def get_config_zone(conf, name=None): + config_path = ['firewall', 'zone'] + if name: + config_path += [name] + + zone_policy = conf.get_config_dict(config_path, key_mangling=('-', '_'), + get_first_key=True, + no_tag_node_value_mangle=True) + return zone_policy + + +def _convert_one_zone_data(zone: str, zone_config: dict) -> dict: + """ + Convert config dictionary of one zone to API dictionary + :param zone: Zone name + :type zone: str + :param zone_config: config dictionary + :type zone_config: dict + :return: AP dictionary + :rtype: dict + """ + list_of_rules = [] + intrazone_dict = {} + if dict_search('from', zone_config): + for from_zone, from_zone_config in zone_config['from'].items(): + from_zone_dict = {'name': from_zone} + if dict_search('firewall.name', from_zone_config): + from_zone_dict['firewall'] = dict_search('firewall.name', + from_zone_config) + if dict_search('firewall.ipv6_name', from_zone_config): + from_zone_dict['firewall_v6'] = dict_search( + 'firewall.ipv6_name', from_zone_config) + list_of_rules.append(from_zone_dict) + + zone_dict = { + 'name': zone, + 'interface': dict_search('interface', zone_config), + 'type': 'LOCAL' if dict_search('local_zone', + zone_config) is not None else None, + } + if list_of_rules: + zone_dict['from'] = list_of_rules + if dict_search('intra_zone_filtering.firewall.name', zone_config): + intrazone_dict['firewall'] = dict_search( + 'intra_zone_filtering.firewall.name', zone_config) + if dict_search('intra_zone_filtering.firewall.ipv6_name', zone_config): + intrazone_dict['firewall_v6'] = dict_search( + 'intra_zone_filtering.firewall.ipv6_name', zone_config) + if intrazone_dict: + zone_dict['intrazone'] = intrazone_dict + return zone_dict + + +def _convert_zones_data(zone_policies: dict) -> list: + """ + Convert all config dictionary to API list of zone dictionaries + :param zone_policies: config dictionary + :type zone_policies: dict + :return: API list + :rtype: list + """ + zone_list = [] + for zone, zone_config in zone_policies.items(): + zone_list.append(_convert_one_zone_data(zone, zone_config)) + return zone_list + + +def _convert_config(zones_config: dict, zone: str = None) -> list: + """ + convert config to API list + :param zones_config: zones config + :type zones_config: + :param zone: zone name + :type zone: str + :return: API list + :rtype: list + """ + if zone: + if zones_config: + output = [_convert_one_zone_data(zone, zones_config)] + else: + raise vyos.opmode.DataUnavailable(f'Zone {zone} not found') + else: + if zones_config: + output = _convert_zones_data(zones_config) + else: + raise vyos.opmode.UnconfiguredSubsystem( + 'Zone entries are not configured') + return output + + +def output_zone_list(zone_conf: dict) -> list: + """ + Format one zone row + :param zone_conf: zone config + :type zone_conf: dict + :return: formatted list of zones + :rtype: list + """ + zone_info = [zone_conf['name']] + if zone_conf['type'] == 'LOCAL': + zone_info.append('LOCAL') + else: + zone_info.append("\n".join(zone_conf['interface'])) + + from_zone = [] + firewall = [] + firewall_v6 = [] + if 'intrazone' in zone_conf: + from_zone.append(zone_conf['name']) + + v4_name = dict_search_args(zone_conf['intrazone'], 'firewall') + v6_name = dict_search_args(zone_conf['intrazone'], 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + if 'from' in zone_conf: + for from_conf in zone_conf['from']: + from_zone.append(from_conf['name']) + + v4_name = dict_search_args(from_conf, 'firewall') + v6_name = dict_search_args(from_conf, 'firewall_v6') + if v4_name: + firewall.append(v4_name) + else: + firewall.append('') + if v6_name: + firewall_v6.append(v6_name) + else: + firewall_v6.append('') + + zone_info.append("\n".join(from_zone)) + zone_info.append("\n".join(firewall)) + zone_info.append("\n".join(firewall_v6)) + return zone_info + + +def get_formatted_output(zone_policy: list) -> str: + """ + Formatted output of all zones + :param zone_policy: list of zones + :type zone_policy: list + :return: formatted table with zones + :rtype: str + """ + headers = ["Zone", + "Interfaces", + "From Zone", + "Firewall IPv4", + "Firewall IPv6" + ] + formatted_list = [] + for zone_conf in zone_policy: + formatted_list.append(output_zone_list(zone_conf)) + tabulate.PRESERVE_WHITESPACE = True + output = tabulate.tabulate(formatted_list, headers, numalign="left") + return output + + +def show(raw: bool, zone: typing.Optional[str]): + """ + Show zone-policy command + :param raw: if API + :type raw: bool + :param zone: zone name + :type zone: str + """ + conf: ConfigTreeQuery = ConfigTreeQuery() + zones_config: dict = get_config_zone(conf, zone) + zone_policy_api: list = _convert_config(zones_config, zone) + if raw: + return zone_policy_api + else: + return get_formatted_output(zone_policy_api) + + +if __name__ == '__main__': + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1)
\ No newline at end of file diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd index e34a4b740..1ba90471e 100755 --- a/src/services/vyos-hostsd +++ b/src/services/vyos-hostsd @@ -271,8 +271,8 @@ SOCKET_PATH = "ipc://" + os.path.join(RUN_DIR, 'vyos-hostsd.sock') RESOLV_CONF_FILE = '/etc/resolv.conf' HOSTS_FILE = '/etc/hosts' -PDNS_REC_USER = PDNS_REC_GROUP = 'pdns' -PDNS_REC_RUN_DIR = '/run/powerdns' +PDNS_REC_USER_GROUP = 'pdns' +PDNS_REC_RUN_DIR = '/run/pdns-recursor' PDNS_REC_LUA_CONF_FILE = f'{PDNS_REC_RUN_DIR}/recursor.vyos-hostsd.conf.lua' PDNS_REC_ZONES_FILE = f'{PDNS_REC_RUN_DIR}/recursor.forward-zones.conf' @@ -436,18 +436,18 @@ def make_hosts(state): def make_pdns_rec_conf(state): logger.info(f"Writing {PDNS_REC_LUA_CONF_FILE}") - # on boot, /run/powerdns does not exist, so create it - makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + # on boot, /run/pdns-recursor does not exist, so create it + makedir(PDNS_REC_RUN_DIR, user=PDNS_REC_USER_GROUP, group=PDNS_REC_USER_GROUP) chmod_755(PDNS_REC_RUN_DIR) render(PDNS_REC_LUA_CONF_FILE, 'dns-forwarding/recursor.vyos-hostsd.conf.lua.j2', - state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + state, user=PDNS_REC_USER_GROUP, group=PDNS_REC_USER_GROUP) logger.info(f"Writing {PDNS_REC_ZONES_FILE}") render(PDNS_REC_ZONES_FILE, 'dns-forwarding/recursor.forward-zones.conf.j2', - state, user=PDNS_REC_USER, group=PDNS_REC_GROUP) + state, user=PDNS_REC_USER_GROUP, group=PDNS_REC_USER_GROUP) def set_host_name(state, data): if data['host_name']: diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index b64e58132..40d442e30 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -1,6 +1,6 @@ #!/usr/share/vyos-http-api-tools/bin/python3 # -# Copyright (C) 2019-2023 VyOS maintainers and contributors +# Copyright (C) 2019-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -13,8 +13,6 @@ # # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -# -# import os import sys @@ -25,6 +23,7 @@ import logging import signal import traceback import threading + from time import sleep from typing import List, Union, Callable, Dict @@ -46,11 +45,12 @@ from ariadne.asgi import GraphQL from vyos.config import Config from vyos.configtree import ConfigTree from vyos.configdiff import get_config_diff -from vyos.configsession import ConfigSession, ConfigSessionError +from vyos.configsession import ConfigSession +from vyos.configsession import ConfigSessionError +from vyos.defaults import api_config_state import api.graphql.state -api_config_state = '/run/http-api-state' CFG_GROUP = 'vyattacfg' debug = True diff --git a/src/system/grub_update.py b/src/system/grub_update.py index 3c851f0e0..5a7d8eb72 100644 --- a/src/system/grub_update.py +++ b/src/system/grub_update.py @@ -68,7 +68,8 @@ if __name__ == '__main__': 'default': grub.gen_version_uuid(default_entry['version']), 'bootmode': default_entry['bootmode'], 'console_type': default_entry['console_type'], - 'console_num': default_entry['console_num'] + 'console_num': default_entry['console_num'], + 'console_speed': default_entry['console_speed'] } vars.update(default_settings) diff --git a/src/system/on-dhcp-event.sh b/src/system/on-dhcp-event.sh index 03574bdc3..52fadd428 100755 --- a/src/system/on-dhcp-event.sh +++ b/src/system/on-dhcp-event.sh @@ -15,28 +15,71 @@ if [ $# -lt 1 ]; then fi action=$1 -client_name=$LEASE4_HOSTNAME -client_ip=$LEASE4_ADDRESS -client_mac=$LEASE4_HWADDR hostsd_client="/usr/bin/vyos-hostsd-client" -case "$action" in - lease4_renew|lease4_recover) # add mapping for new/recovered lease address - if [ -z "$client_name" ]; then - logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" - client_name=$(echo "host-$client_mac" | tr : -) - fi +get_subnet_domain_name () { + python3 <<EOF +from vyos.kea import kea_get_active_config +from vyos.utils.dict import dict_search_args + +config = kea_get_active_config('4') +shared_networks = dict_search_args(config, 'arguments', f'Dhcp4', 'shared-networks') + +found = False - $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply +if shared_networks: + for network in shared_networks: + for subnet in network[f'subnet4']: + if subnet['id'] == $1: + for option in subnet['option-data']: + if option['name'] == 'domain-name': + print(option['data']) + found = True + + if not found: + for option in network['option-data']: + if option['name'] == 'domain-name': + print(option['data']) +EOF +} + +case "$action" in + lease4_renew|lease4_recover) exit 0 ;; lease4_release|lease4_expire|lease4_decline) # delete mapping for released/declined address + client_ip=$LEASE4_ADDRESS $hostsd_client --delete-hosts --tag "dhcp-server-$client_ip" --apply exit 0 ;; - leases4_committed) # nothing to do + leases4_committed) # process committed leases (added/renewed/recovered) + for ((i = 0; i < $LEASES4_SIZE; i++)); do + client_ip_var="LEASES4_AT${i}_ADDRESS" + client_mac_var="LEASES4_AT${i}_HWADDR" + client_name_var="LEASES4_AT${i}_HOSTNAME" + client_subnet_id_var="LEASES4_AT${i}_SUBNET_ID" + + client_ip=${!client_ip_var} + client_mac=${!client_mac_var} + client_name=${!client_name_var%.} + client_subnet_id=${!client_subnet_id_var} + + if [ -z "$client_name" ]; then + logger -s -t on-dhcp-event "Client name was empty, using MAC \"$client_mac\" instead" + client_name=$(echo "host-$client_mac" | tr : -) + fi + + client_domain=$(get_subnet_domain_name $client_subnet_id) + + if [[ -n "$client_domain" ]] && ! [[ $client_name =~ .*$client_domain$ ]]; then + client_name="$client_name.$client_domain" + fi + + $hostsd_client --add-hosts "$client_name,$client_ip" --tag "dhcp-server-$client_ip" --apply + done + exit 0 ;; diff --git a/src/validators/ipv4-range-mask b/src/validators/ipv4-range-mask index 7bb4539af..9373328ff 100755 --- a/src/validators/ipv4-range-mask +++ b/src/validators/ipv4-range-mask @@ -1,12 +1,5 @@ #!/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 @@ -22,37 +15,12 @@ do 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 [[ "${range}" =~ "-" ]]&&[[ ! -z ${mask} ]]; then + ipaddrcheck --range-prefix-length ${mask} --is-ipv4-range ${range} 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 |