summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/pull-request-labels.yml2
-rw-r--r--data/op-mode-standardized.json7
-rw-r--r--data/templates/accel-ppp/config_ip_pool.j220
-rw-r--r--data/templates/accel-ppp/config_ipv6_pool.j221
-rw-r--r--data/templates/accel-ppp/ipoe.config.j24
-rw-r--r--data/templates/accel-ppp/l2tp.config.j231
-rw-r--r--data/templates/accel-ppp/ppp-options.j239
-rw-r--r--data/templates/accel-ppp/pppoe.config.j240
-rw-r--r--data/templates/accel-ppp/pptp.config.j216
-rw-r--r--data/templates/accel-ppp/sstp.config.j218
-rw-r--r--data/templates/chrony/chrony.conf.j210
-rw-r--r--data/templates/container/systemd-unit.j22
-rw-r--r--data/templates/dhcp-server/kea-dhcp4.conf.j28
-rw-r--r--data/templates/dhcp-server/kea-dhcp6.conf.j213
-rw-r--r--data/templates/dns-dynamic/ddclient.conf.j27
-rw-r--r--data/templates/dns-forwarding/override.conf.j28
-rw-r--r--data/templates/dns-forwarding/recursor.conf.j228
-rw-r--r--data/templates/dns-forwarding/recursor.conf.lua.j22
-rw-r--r--data/templates/firewall/nftables-defines.j221
-rw-r--r--data/templates/firewall/nftables-vrf-zones.j217
-rw-r--r--data/templates/firewall/nftables-zone.j213
-rw-r--r--data/templates/firewall/nftables.j24
-rw-r--r--data/templates/firewall/upnpd.conf.j2116
-rw-r--r--data/templates/frr/bfdd.frr.j26
-rw-r--r--data/templates/frr/bgpd.frr.j26
-rw-r--r--data/templates/grub/grub_common.j28
-rw-r--r--data/templates/grub/grub_compat.j212
-rw-r--r--data/templates/grub/grub_vyos_version.j220
-rw-r--r--data/templates/https/nginx.default.j2103
-rw-r--r--data/templates/https/vyos-http-api.service.j22
-rw-r--r--data/templates/ipsec/swanctl/peer.j210
-rw-r--r--data/templates/sflow/override.conf.j23
-rw-r--r--data/vyos-firewall-init.conf19
-rw-r--r--debian/control4
-rwxr-xr-xdebian/rules2
-rw-r--r--git0
-rw-r--r--interface-definitions/container.xml.in24
-rw-r--r--interface-definitions/firewall.xml.in29
-rw-r--r--interface-definitions/include/accel-ppp/client-ip-pool.xml.i8
-rw-r--r--interface-definitions/include/accel-ppp/client-ipv6-pool.xml.i11
-rw-r--r--interface-definitions/include/accel-ppp/default-ipv6-pool.xml.i17
-rw-r--r--interface-definitions/include/accel-ppp/default-pool.xml.i5
-rw-r--r--interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i12
-rw-r--r--interface-definitions/include/accel-ppp/ppp-options.xml.i65
-rw-r--r--interface-definitions/include/bfd/common.xml.i12
-rw-r--r--interface-definitions/include/bgp/afi-nexthop-vpn-export.xml.i32
-rw-r--r--interface-definitions/include/bgp/afi-route-map-export-import.xml.i4
-rw-r--r--interface-definitions/include/bgp/afi-sid.xml.i36
-rw-r--r--interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i8
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i6
-rw-r--r--interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i3
-rw-r--r--interface-definitions/include/constraint/email.xml.i3
-rw-r--r--interface-definitions/include/dhcp/option-v4.xml.i257
-rw-r--r--interface-definitions/include/dhcp/option-v6.xml.i110
-rw-r--r--interface-definitions/include/firewall/add-dynamic-address-groups.xml.i34
-rw-r--r--interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i34
-rw-r--r--interface-definitions/include/firewall/common-rule-inet.xml.i19
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv4.xml.i25
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv6.xml.i25
-rw-r--r--interface-definitions/include/firewall/ipv4-custom-name.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-forward.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv4-hook-input.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-custom-name.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-forward.xml.i1
-rw-r--r--interface-definitions/include/firewall/ipv6-hook-input.xml.i1
-rw-r--r--interface-definitions/include/firewall/match-ipsec.xml.i21
-rw-r--r--interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i17
-rw-r--r--interface-definitions/include/firewall/source-destination-dynamic-group.xml.i17
-rw-r--r--interface-definitions/include/haproxy/rule-backend.xml.i2
-rw-r--r--interface-definitions/include/listen-interface-multi-broadcast.xml.i18
-rw-r--r--interface-definitions/include/pki/dh-params.xml.i10
-rw-r--r--interface-definitions/include/qos/hfsc-m1.xml.i2
-rw-r--r--interface-definitions/include/qos/hfsc-m2.xml.i2
-rw-r--r--interface-definitions/include/route-map.xml.i2
-rw-r--r--interface-definitions/include/version/bgp-version.xml.i2
-rw-r--r--interface-definitions/include/version/conntrack-version.xml.i2
-rw-r--r--interface-definitions/include/version/dhcp-server-version.xml.i2
-rw-r--r--interface-definitions/include/version/dhcpv6-server-version.xml.i2
-rw-r--r--interface-definitions/include/version/dns-dynamic-version.xml.i2
-rw-r--r--interface-definitions/include/version/https-version.xml.i2
-rw-r--r--interface-definitions/include/version/ipoe-server-version.xml.i2
-rw-r--r--interface-definitions/include/version/ipsec-version.xml.i2
-rw-r--r--interface-definitions/include/version/l2tp-version.xml.i2
-rw-r--r--interface-definitions/include/version/pppoe-server-version.xml.i2
-rw-r--r--interface-definitions/include/version/pptp-version.xml.i2
-rw-r--r--interface-definitions/include/version/sstp-version.xml.i2
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in9
-rw-r--r--interface-definitions/pki.xml.in63
-rw-r--r--interface-definitions/policy.xml.in2
-rw-r--r--interface-definitions/service_dhcp-server.xml.in261
-rw-r--r--interface-definitions/service_dhcpv6-server.xml.in185
-rw-r--r--interface-definitions/service_dns_dynamic.xml.in47
-rw-r--r--interface-definitions/service_dns_forwarding.xml.in98
-rw-r--r--interface-definitions/service_https.xml.in107
-rw-r--r--interface-definitions/service_ipoe-server.xml.in1
-rw-r--r--interface-definitions/service_ndp-proxy.xml.in1
-rw-r--r--interface-definitions/service_ntp.xml.in36
-rw-r--r--interface-definitions/service_pppoe-server.xml.in59
-rw-r--r--interface-definitions/service_upnp.xml.in1
-rw-r--r--interface-definitions/system_config-management.xml.in10
-rw-r--r--interface-definitions/system_option.xml.in13
-rw-r--r--interface-definitions/system_sflow.xml.in1
-rw-r--r--interface-definitions/vpn_ipsec.xml.in14
-rw-r--r--interface-definitions/vpn_l2tp.xml.in21
-rw-r--r--interface-definitions/vpn_pptp.xml.in29
-rw-r--r--interface-definitions/vpn_sstp.xml.in14
-rw-r--r--op-mode-definitions/container.xml.in3
-rw-r--r--op-mode-definitions/dns-dynamic.xml.in25
-rw-r--r--op-mode-definitions/dns-forwarding.xml.in20
-rw-r--r--op-mode-definitions/firewall.xml.in132
-rw-r--r--[-rwxr-xr-x]op-mode-definitions/generate-system-login-user.xml.in0
-rw-r--r--op-mode-definitions/monitor-log.xml.in6
-rw-r--r--op-mode-definitions/multicast-group.xml.in63
-rw-r--r--op-mode-definitions/pki.xml.in10
-rw-r--r--op-mode-definitions/rpki.xml.in29
-rw-r--r--op-mode-definitions/show-ip-multicast.xml.in3
-rw-r--r--op-mode-definitions/show-ipv6-route.xml.in19
-rw-r--r--op-mode-definitions/show-log.xml.in6
-rw-r--r--python/vyos/accel_ppp_util.py41
-rw-r--r--python/vyos/config.py51
-rw-r--r--python/vyos/configdict.py13
-rw-r--r--python/vyos/configverify.py7
-rw-r--r--python/vyos/defaults.py12
-rw-r--r--python/vyos/firewall.py24
-rw-r--r--python/vyos/ifconfig/ethernet.py7
-rw-r--r--python/vyos/kea.py117
-rw-r--r--python/vyos/nat.py10
-rw-r--r--python/vyos/opmode.py5
-rw-r--r--python/vyos/qos/base.py50
-rw-r--r--python/vyos/qos/trafficshaper.py101
-rw-r--r--python/vyos/remote.py2
-rw-r--r--python/vyos/system/compat.py25
-rw-r--r--python/vyos/system/disk.py2
-rw-r--r--python/vyos/system/grub.py78
-rw-r--r--python/vyos/system/grub_util.py70
-rw-r--r--python/vyos/system/image.py13
-rw-r--r--python/vyos/template.py39
-rw-r--r--python/vyos/utils/network.py6
-rw-r--r--python/vyos/utils/process.py27
-rw-r--r--smoketest/config-tests/basic-api-service12
-rw-r--r--smoketest/config-tests/basic-vyos9
-rw-r--r--smoketest/config-tests/bgp-medium-confederation73
-rw-r--r--smoketest/config-tests/dialup-router-medium-vpn9
-rw-r--r--smoketest/configs/basic-api-service1
-rw-r--r--smoketest/configs/basic-vyos7
-rw-r--r--smoketest/configs/bgp-azure-ipsec-gateway25
-rw-r--r--smoketest/configs/bgp-medium-confederation247
-rw-r--r--smoketest/configs/ospf-small95
-rw-r--r--smoketest/scripts/cli/base_accel_ppp_test.py120
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py12
-rwxr-xr-xsmoketest/scripts/cli/test_container.py22
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py115
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_ethernet.py13
-rwxr-xr-xsmoketest/scripts/cli/test_nat.py20
-rwxr-xr-xsmoketest/scripts/cli/test_pki.py52
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bfd.py10
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py67
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py185
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py45
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_dynamic.py42
-rwxr-xr-xsmoketest/scripts/cli/test_service_dns_forwarding.py116
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py90
-rwxr-xr-xsmoketest/scripts/cli/test_service_ipoe-server.py45
-rwxr-xr-xsmoketest/scripts/cli/test_service_ntp.py33
-rwxr-xr-xsmoketest/scripts/cli/test_service_pppoe-server.py86
-rwxr-xr-xsmoketest/scripts/cli/test_system_conntrack.py8
-rwxr-xr-xsmoketest/scripts/cli/test_system_sflow.py28
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py1
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_l2tp.py78
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_pptp.py21
-rwxr-xr-xsmoketest/scripts/system/test_module_load.py4
-rwxr-xr-xsrc/conf_mode/container.py14
-rwxr-xr-xsrc/conf_mode/interfaces_ethernet.py15
-rwxr-xr-xsrc/conf_mode/interfaces_openvpn.py16
-rwxr-xr-xsrc/conf_mode/interfaces_sstpc.py6
-rwxr-xr-xsrc/conf_mode/load-balancing_reverse-proxy.py15
-rwxr-xr-xsrc/conf_mode/nat.py9
-rwxr-xr-xsrc/conf_mode/pki.py235
-rwxr-xr-xsrc/conf_mode/policy_route.py4
-rwxr-xr-xsrc/conf_mode/protocols_bfd.py3
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py23
-rwxr-xr-xsrc/conf_mode/protocols_isis.py15
-rwxr-xr-xsrc/conf_mode/protocols_ospf.py13
-rwxr-xr-xsrc/conf_mode/qos.py4
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py39
-rwxr-xr-xsrc/conf_mode/service_dhcpv6-server.py70
-rwxr-xr-xsrc/conf_mode/service_dns_dynamic.py55
-rwxr-xr-xsrc/conf_mode/service_dns_forwarding.py36
-rwxr-xr-xsrc/conf_mode/service_https.py307
-rwxr-xr-xsrc/conf_mode/service_https_certificates_certbot.py114
-rwxr-xr-xsrc/conf_mode/service_ipoe-server.py5
-rwxr-xr-xsrc/conf_mode/service_ntp.py4
-rwxr-xr-xsrc/conf_mode/service_pppoe-server.py3
-rwxr-xr-xsrc/conf_mode/system_console.py26
-rwxr-xr-xsrc/conf_mode/system_option.py11
-rwxr-xr-xsrc/conf_mode/system_sflow.py11
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py33
-rwxr-xr-xsrc/conf_mode/vpn_l2tp.py15
-rwxr-xr-xsrc/conf_mode/vpn_openconnect.py8
-rwxr-xr-xsrc/conf_mode/vpn_pptp.py36
-rwxr-xr-xsrc/conf_mode/vpn_sstp.py15
-rwxr-xr-xsrc/conf_mode/vrf.py47
-rw-r--r--src/etc/dhcp/dhclient-exit-hooks.d/03-vyos-dhclient-hook1
-rwxr-xr-xsrc/etc/dhcp/dhclient-exit-hooks.d/99-ipsec-dhclient-hook4
-rw-r--r--src/etc/sudoers.d/vyos3
-rw-r--r--src/etc/systemd/system/certbot.service.d/10-override.conf7
-rw-r--r--src/etc/systemd/system/frr.service.d/override.conf3
-rw-r--r--src/etc/systemd/system/nginx.service.d/10-override.conf3
-rw-r--r--src/etc/systemd/system/pdns-recursor.service.d/override.conf8
-rwxr-xr-xsrc/helpers/vyos-boot-config-loader.py3
-rwxr-xr-xsrc/helpers/vyos-certbot-renew-pki.sh3
-rwxr-xr-xsrc/migration-scripts/bgp/0-to-12
-rwxr-xr-xsrc/migration-scripts/bgp/1-to-212
-rwxr-xr-xsrc/migration-scripts/bgp/4-to-567
-rwxr-xr-xsrc/migration-scripts/dhcp-server/8-to-975
-rwxr-xr-xsrc/migration-scripts/dhcpv6-server/3-to-490
-rwxr-xr-xsrc/migration-scripts/dns-dynamic/3-to-476
-rwxr-xr-xsrc/migration-scripts/firewall/10-to-1133
-rwxr-xr-xsrc/migration-scripts/https/5-to-6109
-rwxr-xr-xsrc/migration-scripts/ipoe-server/1-to-22
-rwxr-xr-xsrc/migration-scripts/ipoe-server/2-to-361
-rwxr-xr-xsrc/migration-scripts/ipsec/12-to-1359
-rwxr-xr-xsrc/migration-scripts/l2tp/4-to-544
-rwxr-xr-xsrc/migration-scripts/l2tp/6-to-760
-rwxr-xr-xsrc/migration-scripts/l2tp/7-to-868
-rwxr-xr-xsrc/migration-scripts/nat/5-to-65
-rwxr-xr-xsrc/migration-scripts/ospf/0-to-14
-rwxr-xr-xsrc/migration-scripts/policy/4-to-548
-rwxr-xr-xsrc/migration-scripts/pppoe-server/6-to-745
-rwxr-xr-xsrc/migration-scripts/pppoe-server/7-to-861
-rwxr-xr-xsrc/migration-scripts/pppoe-server/8-to-969
-rwxr-xr-xsrc/migration-scripts/pptp/2-to-319
-rwxr-xr-xsrc/migration-scripts/pptp/3-to-451
-rwxr-xr-xsrc/migration-scripts/qos/1-to-248
-rwxr-xr-xsrc/migration-scripts/sstp/4-to-517
-rwxr-xr-xsrc/migration-scripts/sstp/5-to-662
-rwxr-xr-xsrc/op_mode/dhcp.py9
-rwxr-xr-xsrc/op_mode/dns.py170
-rwxr-xr-xsrc/op_mode/dns_dynamic.py113
-rwxr-xr-xsrc/op_mode/dns_forwarding_reset.py54
-rwxr-xr-xsrc/op_mode/dns_forwarding_restart.sh8
-rwxr-xr-xsrc/op_mode/dns_forwarding_statistics.py32
-rwxr-xr-xsrc/op_mode/firewall.py57
-rwxr-xr-xsrc/op_mode/image_installer.py13
-rwxr-xr-xsrc/op_mode/image_manager.py25
-rwxr-xr-xsrc/op_mode/interfaces_wireless.py1
-rwxr-xr-xsrc/op_mode/multicast.py72
-rwxr-xr-xsrc/op_mode/pki.py12
-rwxr-xr-xsrc/op_mode/powerctrl.py4
-rwxr-xr-xsrc/op_mode/show_openvpn.py6
-rw-r--r--src/op_mode/zone.py215
-rwxr-xr-xsrc/services/vyos-hostsd12
-rwxr-xr-xsrc/services/vyos-http-api-server10
-rw-r--r--src/system/grub_update.py3
-rwxr-xr-xsrc/system/on-dhcp-event.sh89
-rwxr-xr-xsrc/system/on-dhcpv6-event.sh87
-rwxr-xr-xsrc/validators/ipv4-range-mask36
257 files changed, 6016 insertions, 2662 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..203a9772e 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
@@ -61,30 +65,8 @@ ip-pool={{ default_pool }}
{# Common chap-secrets and RADIUS server/option definitions #}
{% include 'accel-ppp/config_chap_secrets_radius.j2' %}
-[ppp]
-verbose=1
-check-ip=1
-single-session=replace
-lcp-echo-interval={{ ppp_options.lcp_echo_interval }}
-lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }}
-lcp-echo-failure={{ ppp_options.lcp_echo_failure }}
-{# MTU #}
-mtu={{ mtu }}
-ipv6={{ 'allow' if ppp_options.ipv6 is vyos_defined("deny") and client_ipv6_pool is vyos_defined else ppp_options.ipv6 }}
-ipv4={{ ppp_options.ipv4 }}
-mppe={{ ppp_options.mppe }}
-{% if ccp_disable is vyos_defined %}
-ccp=0
-{% endif %}
-unit-preallocate={{ "1" if authentication.radius.preallocate_vif is vyos_defined else "0" }}
-
-{% if ppp_options.ipv6_intf_id is vyos_defined %}
-ipv6-intf-id={{ ppp_options.ipv6_intf_id }}
-{% endif %}
-{% if ppp_options.ipv6_peer_intf_id is vyos_defined %}
-ipv6-peer-intf-id={{ ppp_options.ipv6_peer_intf_id }}
-{% endif %}
-ipv6-accept-peer-intf-id={{ "1" if ppp_options.ipv6_accept_peer_intf_id is vyos_defined else "0" }}
+{# Common ppp-options definitions #}
+{% include 'accel-ppp/ppp-options.j2' %}
{# Common IPv6 pool definitions #}
{% include 'accel-ppp/config_ipv6_pool.j2' %}
@@ -94,5 +76,4 @@ ipv6-accept-peer-intf-id={{ "1" if ppp_options.ipv6_accept_peer_intf_id is vyos_
[cli]
tcp=127.0.0.1:2004
-sessions-columns=ifname,username,calling-sid,ip,{{ ip6_column | join(',') }}{{ ',' if ip6_column }}rate-limit,type,comp,state,rx-bytes,tx-bytes,uptime
diff --git a/data/templates/accel-ppp/ppp-options.j2 b/data/templates/accel-ppp/ppp-options.j2
new file mode 100644
index 000000000..f2d2519d9
--- /dev/null
+++ b/data/templates/accel-ppp/ppp-options.j2
@@ -0,0 +1,39 @@
+#ppp options
+[ppp]
+verbose=1
+check-ip=1
+ccp={{ "0" if ppp_options.disable_ccp is vyos_defined else "1" }}
+unit-preallocate={{ "1" if authentication.radius.preallocate_vif is vyos_defined else "0" }}
+{% if ppp_options.min_mtu is vyos_defined %}
+min-mtu={{ ppp_options.min_mtu }}
+{% endif %}
+{% if ppp_options.mru is vyos_defined %}
+mru={{ ppp_options.mru }}
+{% endif %}
+mppe={{ ppp_options.mppe }}
+lcp-echo-interval={{ ppp_options.lcp_echo_interval }}
+lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }}
+lcp-echo-failure={{ ppp_options.lcp_echo_failure }}
+{% if ppp_options.ipv4 is vyos_defined %}
+ipv4={{ ppp_options.ipv4 }}
+{% endif %}
+{# IPv6 #}
+{% if ppp_options.ipv6 is vyos_defined %}
+ipv6={{ ppp_options.ipv6 }}
+{% if ppp_options.ipv6_interface_id is vyos_defined %}
+ipv6-intf-id={{ ppp_options.ipv6_interface_id }}
+{% endif %}
+{% if ppp_options.ipv6_peer_interface_id is vyos_defined %}
+{% if ppp_options.ipv6_peer_interface_id == 'ipv4-addr' %}
+ipv6-peer-intf-id=ipv4
+{% else %}
+ipv6-peer-intf-id={{ ppp_options.ipv6_peer_interface_id }}
+{% endif %}
+{% endif %}
+ipv6-accept-peer-intf-id={{ "1" if ppp_options.ipv6_accept_peer_interface_id is vyos_defined else "0" }}
+{% endif %}
+{# MTU #}
+mtu={{ mtu }}
+{% if ppp_options.interface_cache is vyos_defined %}
+unit-cache={{ ppp_options.interface_cache }}
+{% endif %}
diff --git a/data/templates/accel-ppp/pppoe.config.j2 b/data/templates/accel-ppp/pppoe.config.j2
index 4bb1c4450..bf7b2eb72 100644
--- a/data/templates/accel-ppp/pppoe.config.j2
+++ b/data/templates/accel-ppp/pppoe.config.j2
@@ -70,40 +70,8 @@ single-session={{ session_control }}
max-starting={{ max_concurrent_sessions }}
{% endif %}
-[ppp]
-verbose=1
-check-ip=1
-ccp={{ "1" if ppp_options.ccp is vyos_defined else "0" }}
-unit-preallocate={{ "1" if authentication.radius.preallocate_vif is vyos_defined else "0" }}
-{% if ppp_options.min_mtu is vyos_defined %}
-min-mtu={{ ppp_options.min_mtu }}
-{% endif %}
-{% if ppp_options.mru is vyos_defined %}
-mru={{ ppp_options.mru }}
-{% endif %}
-mppe={{ ppp_options.mppe }}
-lcp-echo-interval={{ ppp_options.lcp_echo_interval }}
-lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }}
-lcp-echo-failure={{ ppp_options.lcp_echo_failure }}
-{% if ppp_options.ipv4 is vyos_defined %}
-ipv4={{ ppp_options.ipv4 }}
-{% endif %}
-{# IPv6 #}
-{% if ppp_options.ipv6 is vyos_defined %}
-ipv6={{ ppp_options.ipv6 }}
-{% if ppp_options.ipv6_intf_id is vyos_defined %}
-ipv6-intf-id={{ ppp_options.ipv6_intf_id }}
-{% endif %}
-{% if ppp_options.ipv6_peer_intf_id is vyos_defined %}
-ipv6-peer-intf-id={{ ppp_options.ipv6_peer_intf_id }}
-{% endif %}
-ipv6-accept-peer-intf-id={{ "1" if ppp_options.ipv6_accept_peer_intf_id is vyos_defined else "0" }}
-{% endif %}
-{# MTU #}
-mtu={{ mtu }}
-{% if ppp_options.interface_cache is vyos_defined %}
-unit-cache={{ ppp_options.interface_cache }}
-{% endif %}
+{# Common ppp-options definitions #}
+{% include 'accel-ppp/ppp-options.j2' %}
[pppoe]
verbose=1
@@ -143,6 +111,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/pptp.config.j2 b/data/templates/accel-ppp/pptp.config.j2
index daafd6e92..7fe4b17bf 100644
--- a/data/templates/accel-ppp/pptp.config.j2
+++ b/data/templates/accel-ppp/pptp.config.j2
@@ -6,6 +6,8 @@ shaper
{# Common authentication backend definitions #}
{% include 'accel-ppp/config_modules_auth_mode.j2' %}
ippool
+{# Common IPv6 definitions #}
+{% include 'accel-ppp/config_modules_ipv6.j2' %}
{# Common authentication protocols (pap, chap ...) #}
{% if authentication.require is vyos_defined %}
{% if authentication.require == 'chap' %}
@@ -40,7 +42,6 @@ wins{{ loop.index }}={{ server }}
{% endfor %}
{% endif %}
-
[pptp]
ifname=pptp%d
{% if outside_address is vyos_defined %}
@@ -54,6 +55,10 @@ echo-failure=3
{% 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
@@ -61,10 +66,11 @@ ip-pool={{ default_pool }}
{# Common IP pool definitions #}
{% include 'accel-ppp/config_ip_pool.j2' %}
-[ppp]
-verbose=5
-check-ip=1
-single-session=replace
+{# Common IPv6 pool definitions #}
+{% include 'accel-ppp/config_ipv6_pool.j2' %}
+
+{# Common ppp-options definitions #}
+{% include 'accel-ppp/ppp-options.j2' %}
{# Common chap-secrets and RADIUS server/option definitions #}
{% include 'accel-ppp/config_chap_secrets_radius.j2' %}
diff --git a/data/templates/accel-ppp/sstp.config.j2 b/data/templates/accel-ppp/sstp.config.j2
index 014ae1235..c0bc62d9f 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' %}
@@ -52,18 +56,8 @@ ip-pool={{ default_pool }}
{# Common chap-secrets and RADIUS server/option definitions #}
{% include 'accel-ppp/config_chap_secrets_radius.j2' %}
-[ppp]
-verbose=1
-check-ip=1
-{# MTU #}
-mtu={{ mtu }}
-unit-preallocate={{ "1" if authentication.radius.preallocate_vif is vyos_defined else "0" }}
-ipv6={{ 'allow' if ppp_options.ipv6 is vyos_defined("deny") and client_ipv6_pool is vyos_defined else ppp_options.ipv6 }}
-ipv4={{ ppp_options.ipv4 }}
-mppe={{ ppp_options.mppe }}
-lcp-echo-interval={{ ppp_options.lcp_echo_interval }}
-lcp-echo-timeout={{ ppp_options.lcp_echo_timeout }}
-lcp-echo-failure={{ ppp_options.lcp_echo_failure }}
+{# Common ppp-options definitions #}
+{% include 'accel-ppp/ppp-options.j2' %}
{# Common RADIUS shaper configuration #}
{% include 'accel-ppp/config_shaper_radius.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..2f0de6b30 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
},
@@ -15,6 +19,15 @@
"name": "{{ lease_file }}"
},
"hooks-libraries": [
+{% if disable_route_autoinstall is not vyos_defined %}
+ {
+ "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_run_script.so",
+ "parameters": {
+ "name": "/usr/libexec/vyos/system/on-dhcpv6-event.sh",
+ "sync": false
+ }
+ },
+{% endif %}
{
"library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so",
"parameters": {}
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-defines.j2 b/data/templates/firewall/nftables-defines.j2
index a20c399ae..8a75ab2d6 100644
--- a/data/templates/firewall/nftables-defines.j2
+++ b/data/templates/firewall/nftables-defines.j2
@@ -98,5 +98,26 @@
}
{% endfor %}
{% endif %}
+
+{% if group.dynamic_group is vyos_defined %}
+{% if group.dynamic_group.address_group is vyos_defined and not is_ipv6 and is_l3 %}
+{% for group_name, group_conf in group.dynamic_group.address_group.items() %}
+ set DA_{{ group_name }} {
+ type {{ ip_type }}
+ flags dynamic, timeout
+ }
+{% endfor %}
+{% endif %}
+
+{% if group.dynamic_group.ipv6_address_group is vyos_defined and is_ipv6 and is_l3 %}
+{% for group_name, group_conf in group.dynamic_group.ipv6_address_group.items() %}
+ set DA6_{{ group_name }} {
+ type {{ ip_type }}
+ flags dynamic, timeout
+ }
+{% endfor %}
+{% endif %}
+{% endif %}
+
{% endif %}
{% endmacro %}
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/firewall/upnpd.conf.j2 b/data/templates/firewall/upnpd.conf.j2
index e964fc696..616e8869f 100644
--- a/data/templates/firewall/upnpd.conf.j2
+++ b/data/templates/firewall/upnpd.conf.j2
@@ -3,13 +3,42 @@
# WAN network interface
ext_ifname={{ wan_interface }}
{% if wan_ip is vyos_defined %}
+
+# if the WAN network interface for IPv6 is different than for IPv4,
+# set ext_ifname6
+#ext_ifname6=eth2
+
# If the WAN interface has several IP addresses, you
-# can specify the one to use below
+# can specify the one to use below.
+# Setting ext_ip is also useful in double NAT setup, you can declare here
+# the public IP address.
{% for addr in wan_ip %}
ext_ip={{ addr }}
{% endfor %}
{% endif %}
+{% if stun is vyos_defined %}
+# WAN interface must have public IP address. Otherwise it is behind NAT
+# and port forwarding is impossible. In some cases WAN interface can be
+# behind unrestricted full-cone NAT 1:1 when all incoming traffic is NAT-ed and
+# routed to WAN interfaces without any filtering. In this cases miniupnpd
+# needs to know public IP address and it can be learnt by asking external
+# server via STUN protocol. Following option enable retrieving external
+# public IP address from STUN server and detection of NAT type. You need
+# to specify also external STUN server in stun_host option below.
+# This option is disabled by default.
+ext_perform_stun=yes
+# Specify STUN server, either hostname or IP address
+# Some public STUN servers:
+# stun.stunprotocol.org
+# stun.sipgate.net
+# stun.xten.com
+# stun.l.google.com (on non standard port 19302)
+ext_stun_host={{ stun.host }}
+# Specify STUN UDP port, by default it is standard port 3478.
+ext_stun_port={{ stun.port }}
+{% endif %}
+
# LAN network interfaces IPs / networks
{% if listen is vyos_defined %}
# There can be multiple listening IPs for SSDP traffic, in that case
@@ -20,6 +49,9 @@ ext_ip={{ addr }}
# When MULTIPLE_EXTERNAL_IP is enabled, the external IP
# address associated with the subnet follows. For example:
# listening_ip=192.168.0.1/24 88.22.44.13
+# When MULTIPLE_EXTERNAL_IP is disabled, you can list associated network
+# interfaces (for bridges)
+# listening_ip=bridge0 em0 wlan0
{% for addr in listen %}
{% if addr | is_ipv4 %}
listening_ip={{ addr }}
@@ -65,6 +97,18 @@ min_lifetime={{ pcp_lifetime.min }}
{% endif %}
{% endif %}
+# table names for netfilter nft. Default is "filter" for both
+#upnp_table_name=
+#upnp_nat_table_name=
+# chain names for netfilter and netfilter nft
+# netfilter : default are MINIUPNPD, MINIUPNPD, MINIUPNPD-POSTROUTING
+# netfilter nft : default are miniupnpd, prerouting_miniupnpd, postrouting_miniupnpd
+#upnp_forward_chain=forwardUPnP
+#upnp_nat_chain=UPnP
+#upnp_nat_postrouting_chain=UPnP-Postrouting
+
+# Lease file location
+lease_file=/config/upnp.leases
# To enable the next few runtime options, see compile time
# ENABLE_MANUFACTURER_INFO_CONFIGURATION (config.h)
@@ -89,6 +133,11 @@ model_description=Vyos open source enterprise router/firewall operating system
# Model URL, default is URL of OS vendor
model_url=https://vyos.io/
+# Bitrates reported by daemon in bits per second
+# by default miniupnpd tries to get WAN interface speed
+#bitrate_up=1000000
+#bitrate_down=10000000
+
{% if secure_mode is vyos_defined %}
# Secure Mode, UPnP clients can only add mappings to their own IP
secure_mode=yes
@@ -108,6 +157,10 @@ secure_mode=no
# Report system uptime instead of daemon uptime
system_uptime=yes
+# Notify interval in seconds. default is 30 seconds.
+#notify_interval=240
+notify_interval=60
+
# Unused rules cleaning.
# never remove any rule before this threshold for the number
# of redirections is exceeded. default to 20
@@ -116,25 +169,46 @@ clean_ruleset_threshold=10
# a 600 seconds (10 minutes) interval makes sense
clean_ruleset_interval=600
+############################################################################
+## The next 5 config parameters (packet_log, anchor, queue, tag, quickrules)
+## are specific to BSD's pf(4) packet filter and hence cannot be enabled in
+## VyOS.
+# Log packets in pf (default is no)
+#packet_log=no
+
# Anchor name in pf (default is miniupnpd)
-# Something wrong with this option "anchor", comment it out
-# vyos@r14# miniupnpd -vv -f /run/upnp/miniupnp.conf
-# invalid option in file /run/upnp/miniupnp.conf line 74 : anchor=VyOS
-#anchor=VyOS
+#anchor=miniupnpd
-uuid={{ uuid }}
+# ALTQ queue in pf
+# Filter rules must be used for this to be used.
+# compile with PF_ENABLE_FILTER_RULES (see config.h file)
+#queue=queue_name1
-# Lease file location
-lease_file=/config/upnp.leases
+# Tag name in pf
+#tag=tag_name1
+
+# Make filter rules in pf quick or not. default is yes
+# active when compiled with PF_ENABLE_FILTER_RULES (see config.h file)
+#quickrules=no
+##
+## End of pf(4)-specific configuration not to be set in VyOS.
+############################################################################
+
+# UUID, generate your own UUID with "make genuuid"
+uuid={{ uuid }}
# Daemon's serial and model number when reporting to clients
# (in XML description)
#serial=12345678
#model_number=1
+# If compiled with IGD_V2 defined, force reporting IGDv1 in rootDesc (default
+# is no)
+#force_igd_desc_v1=no
+
{% if rule is vyos_defined %}
-# UPnP permission rules
-# (allow|deny) (external port range) IP/mask (internal port range)
+# UPnP permission rules (also enforced for NAT-PMP and PCP)
+# (allow|deny) (external port range) IP/mask (internal port range) (optional regex filter)
# A port range is <min port>-<max port> or <port> if there is only
# one port in the range.
# IP/mask format must be nnn.nnn.nnn.nnn/nn
@@ -151,25 +225,3 @@ lease_file=/config/upnp.leases
{% endif %}
{% endfor %}
{% endif %}
-
-{% if stun is vyos_defined %}
-# WAN interface must have public IP address. Otherwise it is behind NAT
-# and port forwarding is impossible. In some cases WAN interface can be
-# behind unrestricted NAT 1:1 when all incoming traffic is NAT-ed and
-# routed to WAN interfaces without any filtering. In this cases miniupnpd
-# needs to know public IP address and it can be learnt by asking external
-# server via STUN protocol. Following option enable retrieving external
-# public IP address from STUN server and detection of NAT type. You need
-# to specify also external STUN server in stun_host option below.
-# This option is disabled by default.
-ext_perform_stun=yes
-# Specify STUN server, either hostname or IP address
-# Some public STUN servers:
-# stun.stunprotocol.org
-# stun.sipgate.net
-# stun.xten.com
-# stun.l.google.com (on non standard port 19302)
-ext_stun_host={{ stun.host }}
-# Specify STUN UDP port, by default it is standard port 3478.
-ext_stun_port={{ stun.port }}
-{% endif %}
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/git b/git
deleted file mode 100644
index e69de29bb..000000000
--- a/git
+++ /dev/null
diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in
index b35ba8d1c..f0db8a6f2 100644
--- a/interface-definitions/container.xml.in
+++ b/interface-definitions/container.xml.in
@@ -316,6 +316,30 @@
</properties>
<defaultValue>on-failure</defaultValue>
</leafNode>
+ <leafNode name="uid">
+ <properties>
+ <help>User ID this container will run as</help>
+ <valueHelp>
+ <format>u32:0-65535</format>
+ <description>User ID this container will run as</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="gid">
+ <properties>
+ <help>Group ID this container will run as</help>
+ <valueHelp>
+ <format>u32:0-65535</format>
+ <description>Group ID this container will run as</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 0-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
<tagNode name="volume">
<properties>
<help>Mount a volume into the container</help>
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index a4023058f..662ba24ab 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -115,6 +115,35 @@
#include <include/generic-description.xml.i>
</children>
</tagNode>
+ <node name="dynamic-group">
+ <properties>
+ <help>Firewall dynamic group</help>
+ </properties>
+ <children>
+ <tagNode name="address-group">
+ <properties>
+ <help>Firewall dynamic address group</help>
+ <constraint>
+ <regex>[a-zA-Z0-9][\w\-\.]*</regex>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="ipv6-address-group">
+ <properties>
+ <help>Firewall dynamic IPv6 address group</help>
+ <constraint>
+ <regex>[a-zA-Z0-9][\w\-\.]*</regex>
+ </constraint>
+ </properties>
+ <children>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
<tagNode name="interface-group">
<properties>
<help>Firewall interface-group</help>
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 dff574e6c..b30a5ee01 100644
--- a/interface-definitions/include/accel-ppp/client-ip-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/client-ip-pool.xml.i
@@ -7,7 +7,7 @@
<description>Name of IP pool</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
</properties>
<children>
@@ -27,17 +27,21 @@
<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>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
</properties>
</leafNode>
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 832594c12..e06642c37 100644
--- a/interface-definitions/include/accel-ppp/default-pool.xml.i
+++ b/interface-definitions/include/accel-ppp/default-pool.xml.i
@@ -2,12 +2,15 @@
<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>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
</properties>
</leafNode>
diff --git a/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i b/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i
index 265f7f97c..c4cf0a458 100644
--- a/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i
+++ b/interface-definitions/include/accel-ppp/ppp-options-ipv6-interface-id.xml.i
@@ -1,5 +1,5 @@
<!-- include start from accel-ppp/ppp-options-ipv6-interface-id.xml.i -->
-<leafNode name="ipv6-intf-id">
+<leafNode name="ipv6-interface-id">
<properties>
<help>Fixed or random interface identifier for IPv6</help>
<completionHelp>
@@ -18,11 +18,11 @@
</constraint>
</properties>
</leafNode>
-<leafNode name="ipv6-peer-intf-id">
+<leafNode name="ipv6-peer-interface-id">
<properties>
<help>Peer interface identifier for IPv6</help>
<completionHelp>
- <list>random calling-sid ipv4</list>
+ <list>random calling-sid ipv4-addr</list>
</completionHelp>
<valueHelp>
<format>x:x:x:x</format>
@@ -33,7 +33,7 @@
<description>Use a random interface identifier for IPv6</description>
</valueHelp>
<valueHelp>
- <format>ipv4</format>
+ <format>ipv4-addr</format>
<description>Calculate interface identifier from IPv4 address, for example 192:168:0:1</description>
</valueHelp>
<valueHelp>
@@ -41,11 +41,11 @@
<description>Calculate interface identifier from calling-station-id</description>
</valueHelp>
<constraint>
- <regex>(random|calling-sid|ipv4|((\d+){1,4}:){3}(\d+){1,4})</regex>
+ <regex>(random|calling-sid|ipv4-addr|((\d+){1,4}:){3}(\d+){1,4})</regex>
</constraint>
</properties>
</leafNode>
-<leafNode name="ipv6-accept-peer-intf-id">
+<leafNode name="ipv6-accept-peer-interface-id">
<properties>
<help>Accept peer interface identifier</help>
<valueless/>
diff --git a/interface-definitions/include/accel-ppp/ppp-options.xml.i b/interface-definitions/include/accel-ppp/ppp-options.xml.i
new file mode 100644
index 000000000..9b4f1d0ca
--- /dev/null
+++ b/interface-definitions/include/accel-ppp/ppp-options.xml.i
@@ -0,0 +1,65 @@
+<!-- include start from accel-ppp/ppp-options.xml.i -->
+<node name="ppp-options">
+ <properties>
+ <help>Advanced protocol options</help>
+ </properties>
+ <children>
+ <leafNode name="min-mtu">
+ <properties>
+ <help>Minimum acceptable MTU (68-65535)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 68-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="mru">
+ <properties>
+ <help>Preferred MRU (68-65535)</help>
+ <constraint>
+ <validator name="numeric" argument="--range 68-65535"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-ccp">
+ <properties>
+ <help>Disable Compression Control Protocol (CCP)</help>
+ <valueless />
+ </properties>
+ </leafNode>
+ #include <include/accel-ppp/ppp-mppe.xml.i>
+ #include <include/accel-ppp/lcp-echo-interval-failure.xml.i>
+ #include <include/accel-ppp/lcp-echo-timeout.xml.i>
+ #include <include/accel-ppp/ppp-interface-cache.xml.i>
+ <leafNode name="ipv4">
+ <properties>
+ <help>IPv4 (IPCP) negotiation algorithm</help>
+ <constraint>
+ <regex>(deny|allow|prefer|require)</regex>
+ </constraint>
+ <constraintErrorMessage>invalid value</constraintErrorMessage>
+ <valueHelp>
+ <format>deny</format>
+ <description>Do not negotiate IPv4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>allow</format>
+ <description>Negotiate IPv4 only if client requests</description>
+ </valueHelp>
+ <valueHelp>
+ <format>prefer</format>
+ <description>Ask client for IPv4 negotiation, do not fail if it rejects</description>
+ </valueHelp>
+ <valueHelp>
+ <format>require</format>
+ <description>Require IPv4 negotiation</description>
+ </valueHelp>
+ <completionHelp>
+ <list>deny allow prefer require</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ #include <include/accel-ppp/ppp-options-ipv6.xml.i>
+ #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i>
+ </children>
+</node>
+<!-- include end -->
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-route-map-export-import.xml.i b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
index c218937c8..388991241 100644
--- a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
@@ -10,7 +10,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
@@ -26,7 +26,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
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/neighbor-afi-ipv4-ipv6-common.xml.i b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
index 9ec513da9..c8ad68700 100644
--- a/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
+++ b/interface-definitions/include/bgp/neighbor-afi-ipv4-ipv6-common.xml.i
@@ -28,7 +28,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
@@ -44,7 +44,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
@@ -60,7 +60,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
@@ -185,7 +185,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
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/alpha-numeric-hyphen-underscore-dot.xml.i b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i
new file mode 100644
index 000000000..7aeb85260
--- /dev/null
+++ b/interface-definitions/include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i
@@ -0,0 +1,3 @@
+<!-- include start from constraint/alpha-numeric-hyphen-underscore-dot.xml.i -->
+<regex>[-_a-zA-Z0-9.]+</regex>
+<!-- include end -->
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/add-dynamic-address-groups.xml.i b/interface-definitions/include/firewall/add-dynamic-address-groups.xml.i
new file mode 100644
index 000000000..769761cb6
--- /dev/null
+++ b/interface-definitions/include/firewall/add-dynamic-address-groups.xml.i
@@ -0,0 +1,34 @@
+<!-- include start from firewall/add-dynamic-address-groups.xml.i -->
+<leafNode name="address-group">
+ <properties>
+ <help>Dynamic address-group</help>
+ <completionHelp>
+ <path>firewall group dynamic-group address-group</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<leafNode name="timeout">
+ <properties>
+ <help>Set timeout</help>
+ <valueHelp>
+ <format>&lt;number&gt;s</format>
+ <description>Timeout value in seconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;m</format>
+ <description>Timeout value in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;h</format>
+ <description>Timeout value in hours</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;d</format>
+ <description>Timeout value in days</description>
+ </valueHelp>
+ <constraint>
+ <regex>\d+(s|m|h|d)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i b/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i
new file mode 100644
index 000000000..7bd91c58a
--- /dev/null
+++ b/interface-definitions/include/firewall/add-dynamic-ipv6-address-groups.xml.i
@@ -0,0 +1,34 @@
+<!-- include start from firewall/add-dynamic-ipv6-address-groups.xml.i -->
+<leafNode name="address-group">
+ <properties>
+ <help>Dynamic ipv6-address-group</help>
+ <completionHelp>
+ <path>firewall group dynamic-group ipv6-address-group</path>
+ </completionHelp>
+ </properties>
+</leafNode>
+<leafNode name="timeout">
+ <properties>
+ <help>Set timeout</help>
+ <valueHelp>
+ <format>&lt;number&gt;s</format>
+ <description>Timeout value in seconds</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;m</format>
+ <description>Timeout value in minutes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;h</format>
+ <description>Timeout value in hours</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;number&gt;d</format>
+ <description>Timeout value in days</description>
+ </valueHelp>
+ <constraint>
+ <regex>\d+(s|m|h|d)</regex>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end --> \ No newline at end of file
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/common-rule-ipv4.xml.i b/interface-definitions/include/firewall/common-rule-ipv4.xml.i
index 4ed179ae7..158c7a662 100644
--- a/interface-definitions/include/firewall/common-rule-ipv4.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv4.xml.i
@@ -1,6 +1,29 @@
<!-- include start from firewall/common-rule-ipv4.xml.i -->
#include <include/firewall/common-rule-inet.xml.i>
#include <include/firewall/ttl.xml.i>
+<node name="add-address-to-group">
+ <properties>
+ <help>Add ip address to dynamic address-group</help>
+ </properties>
+ <children>
+ <node name="source-address">
+ <properties>
+ <help>Add source ip addresses to dynamic address-group</help>
+ </properties>
+ <children>
+ #include <include/firewall/add-dynamic-address-groups.xml.i>
+ </children>
+ </node>
+ <node name="destination-address">
+ <properties>
+ <help>Add destination ip addresses to dynamic address-group</help>
+ </properties>
+ <children>
+ #include <include/firewall/add-dynamic-address-groups.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
<node name="destination">
<properties>
<help>Destination parameters</help>
@@ -13,6 +36,7 @@
#include <include/firewall/mac-address.xml.i>
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
+ #include <include/firewall/source-destination-dynamic-group.xml.i>
</children>
</node>
<node name="icmp">
@@ -67,6 +91,7 @@
#include <include/firewall/mac-address.xml.i>
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
+ #include <include/firewall/source-destination-dynamic-group.xml.i>
</children>
</node>
<!-- include end --> \ No newline at end of file
diff --git a/interface-definitions/include/firewall/common-rule-ipv6.xml.i b/interface-definitions/include/firewall/common-rule-ipv6.xml.i
index 6219557db..78eeb361e 100644
--- a/interface-definitions/include/firewall/common-rule-ipv6.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv6.xml.i
@@ -1,6 +1,29 @@
<!-- include start from firewall/common-rule-ipv6.xml.i -->
#include <include/firewall/common-rule-inet.xml.i>
#include <include/firewall/hop-limit.xml.i>
+<node name="add-address-to-group">
+ <properties>
+ <help>Add ipv6 address to dynamic ipv6-address-group</help>
+ </properties>
+ <children>
+ <node name="source-address">
+ <properties>
+ <help>Add source ipv6 addresses to dynamic ipv6-address-group</help>
+ </properties>
+ <children>
+ #include <include/firewall/add-dynamic-ipv6-address-groups.xml.i>
+ </children>
+ </node>
+ <node name="destination-address">
+ <properties>
+ <help>Add destination ipv6 addresses to dynamic ipv6-address-group</help>
+ </properties>
+ <children>
+ #include <include/firewall/add-dynamic-ipv6-address-groups.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
<node name="destination">
<properties>
<help>Destination parameters</help>
@@ -13,6 +36,7 @@
#include <include/firewall/mac-address.xml.i>
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group-ipv6.xml.i>
+ #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i>
</children>
</node>
<node name="icmpv6">
@@ -67,6 +91,7 @@
#include <include/firewall/mac-address.xml.i>
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group-ipv6.xml.i>
+ #include <include/firewall/source-destination-dynamic-group-ipv6.xml.i>
</children>
</node>
<!-- include end --> \ No newline at end of file
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/firewall/source-destination-dynamic-group-ipv6.xml.i b/interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i
new file mode 100644
index 000000000..845f8fe7c
--- /dev/null
+++ b/interface-definitions/include/firewall/source-destination-dynamic-group-ipv6.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from firewall/source-destination-dynamic-group-ipv6.xml.i -->
+<node name="group">
+ <properties>
+ <help>Group</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic-address-group">
+ <properties>
+ <help>Group of dynamic ipv6 addresses</help>
+ <completionHelp>
+ <path>firewall group dynamic-group ipv6-address-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i b/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i
new file mode 100644
index 000000000..29ab98c68
--- /dev/null
+++ b/interface-definitions/include/firewall/source-destination-dynamic-group.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from firewall/source-destination-dynamic-group.xml.i -->
+<node name="group">
+ <properties>
+ <help>Group</help>
+ </properties>
+ <children>
+ <leafNode name="dynamic-address-group">
+ <properties>
+ <help>Group of dynamic addresses</help>
+ <completionHelp>
+ <path>firewall group dynamic-group address-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
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..00bd45e6e
--- /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 to listen on</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/route-map.xml.i b/interface-definitions/include/route-map.xml.i
index 019868373..e49c388d6 100644
--- a/interface-definitions/include/route-map.xml.i
+++ b/interface-definitions/include/route-map.xml.i
@@ -10,7 +10,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
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..01004c5a0 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='8'></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..c253c58d9 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='9'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/pptp-version.xml.i b/interface-definitions/include/version/pptp-version.xml.i
index 4386cedbd..3e1482ecc 100644
--- a/interface-definitions/include/version/pptp-version.xml.i
+++ b/interface-definitions/include/version/pptp-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/pptp-version.xml.i -->
-<syntaxVersion component='pptp' version='3'></syntaxVersion>
+<syntaxVersion component='pptp' version='4'></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 3449819be..0ed199539 100644
--- a/interface-definitions/pki.xml.in
+++ b/interface-definitions/pki.xml.in
@@ -9,6 +9,9 @@
<tagNode name="ca">
<properties>
<help>Certificate Authority</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
</properties>
<children>
<leafNode name="certificate">
@@ -64,6 +67,9 @@
<tagNode name="certificate">
<properties>
<help>Certificate</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
</properties>
<children>
<leafNode name="certificate">
@@ -75,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>
@@ -109,6 +169,9 @@
<tagNode name="dh">
<properties>
<help>Diffie-Hellman parameters</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
</properties>
<children>
<leafNode name="parameters">
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index 0d2ed9746..0d82cd3f8 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -476,7 +476,7 @@
<description>Route map name</description>
</valueHelp>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Name of route-map can only contain alpha-numeric letters, hyphen and underscores</constraintErrorMessage>
</properties>
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index e35d845f1..5c9d4a360 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -38,7 +38,7 @@
<properties>
<help>Peer name used to identify connection</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Invalid failover peer name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
@@ -74,11 +74,12 @@
</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>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
@@ -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,54 +136,16 @@
</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>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #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-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 102c164a6..28b97a64b 100644
--- a/interface-definitions/service_dhcpv6-server.xml.in
+++ b/interface-definitions/service_dhcpv6-server.xml.in
@@ -9,6 +9,13 @@
</properties>
<children>
#include <include/generic-disable-node.xml.i>
+ #include <include/listen-interface-multi-broadcast.xml.i>
+ <leafNode name="disable-route-autoinstall">
+ <properties>
+ <help>Do not install routes for delegated prefixes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<node name="global-parameters">
<properties>
<help>Additional global parameters for DHCPv6 server</help>
@@ -34,7 +41,7 @@
<properties>
<help>DHCPv6 shared network name</help>
<constraint>
- <regex>[-_a-zA-Z0-9.]+</regex>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
</constraint>
<constraintErrorMessage>Invalid DHCPv6 shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
@@ -89,11 +96,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 +117,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 +130,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 +188,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>
- <regex>[-_a-zA-Z0-9.]+</regex>
- </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>
- <regex>[-_a-zA-Z0-9.]+</regex>
- </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>
@@ -268,37 +231,35 @@
<constraintErrorMessage>Delegated prefix length must be between 32 and 96</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="excluded-prefix">
+ <properties>
+ <help>IPv6 prefix to be excluded from prefix delegation</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 prefix excluded from prefix delegation</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="excluded-prefix-length">
+ <properties>
+ <help>Length in bits of excluded prefix</help>
+ <valueHelp>
+ <format>u32:33-64</format>
+ <description>Excluded prefix length (33-128)</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 33-128"/>
+ </constraint>
+ <constraintErrorMessage>Prefix length must be between 33 and 128</constraintErrorMessage>
+ </properties>
+ </leafNode>
</children>
</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 +269,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 +299,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..477ed115f 100644
--- a/interface-definitions/service_pppoe-server.xml.in
+++ b/interface-definitions/service_pppoe-server.xml.in
@@ -103,68 +103,12 @@
</properties>
</leafNode>
#include <include/accel-ppp/wins-server.xml.i>
+ #include <include/accel-ppp/ppp-options.xml.i>
<node name="ppp-options">
- <properties>
- <help>Advanced protocol options</help>
- </properties>
<children>
<leafNode name="min-mtu">
- <properties>
- <help>Minimum acceptable MTU (68-65535)</help>
- <constraint>
- <validator name="numeric" argument="--range 68-65535"/>
- </constraint>
- </properties>
<defaultValue>1280</defaultValue>
</leafNode>
- <leafNode name="mru">
- <properties>
- <help>Preferred MRU (68-65535)</help>
- <constraint>
- <validator name="numeric" argument="--range 68-65535"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="ccp">
- <properties>
- <help>CCP negotiation (default disabled)</help>
- <valueless />
- </properties>
- </leafNode>
- #include <include/accel-ppp/ppp-mppe.xml.i>
- #include <include/accel-ppp/lcp-echo-interval-failure.xml.i>
- #include <include/accel-ppp/lcp-echo-timeout.xml.i>
- #include <include/accel-ppp/ppp-interface-cache.xml.i>
- <leafNode name="ipv4">
- <properties>
- <help>IPv4 (IPCP) negotiation algorithm</help>
- <constraint>
- <regex>(deny|allow|prefer|require)</regex>
- </constraint>
- <constraintErrorMessage>invalid value</constraintErrorMessage>
- <valueHelp>
- <format>deny</format>
- <description>Do not negotiate IPv4</description>
- </valueHelp>
- <valueHelp>
- <format>allow</format>
- <description>Negotiate IPv4 only if client requests</description>
- </valueHelp>
- <valueHelp>
- <format>prefer</format>
- <description>Ask client for IPv4 negotiation, do not fail if it rejects</description>
- </valueHelp>
- <valueHelp>
- <format>require</format>
- <description>Require IPv4 negotiation</description>
- </valueHelp>
- <completionHelp>
- <list>deny allow prefer require</list>
- </completionHelp>
- </properties>
- </leafNode>
- #include <include/accel-ppp/ppp-options-ipv6.xml.i>
- #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i>
</children>
</node>
<tagNode name="pado-delay">
@@ -274,6 +218,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_upnp.xml.in b/interface-definitions/service_upnp.xml.in
index 20e01bfbd..064386ee5 100644
--- a/interface-definitions/service_upnp.xml.in
+++ b/interface-definitions/service_upnp.xml.in
@@ -205,6 +205,7 @@
<constraint>
<validator name="ipv4-address"/>
<validator name="ipv4-host"/>
+ <validator name="ipv4-prefix"/>
</constraint>
</properties>
</leafNode>
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..942690bca 100644
--- a/interface-definitions/vpn_l2tp.xml.in
+++ b/interface-definitions/vpn_l2tp.xml.in
@@ -49,12 +49,6 @@
</leafNode>
</children>
</node>
- <leafNode name="ccp-disable">
- <properties>
- <help>Disable Compression Control Protocol (CCP)</help>
- <valueless />
- </properties>
- </leafNode>
<node name="ipsec-settings">
<properties>
<help>Internet Protocol Security (IPsec) for remote access L2TP VPN</help>
@@ -140,20 +134,9 @@
</node>
</children>
</node>
- <node name="ppp-options">
- <properties>
- <help>Advanced protocol options</help>
- </properties>
- <children>
- #include <include/accel-ppp/ppp-mppe.xml.i>
- #include <include/accel-ppp/ppp-options-ipv4.xml.i>
- #include <include/accel-ppp/ppp-options-ipv6.xml.i>
- #include <include/accel-ppp/ppp-options-ipv6-interface-id.xml.i>
- #include <include/accel-ppp/lcp-echo-interval-failure.xml.i>
- #include <include/accel-ppp/lcp-echo-timeout.xml.i>
- </children>
- </node>
+ #include <include/accel-ppp/ppp-options.xml.i>
#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..d23086c02 100644
--- a/interface-definitions/vpn_pptp.xml.in
+++ b/interface-definitions/vpn_pptp.xml.in
@@ -27,7 +27,7 @@
</properties>
</leafNode>
#include <include/accel-ppp/gateway-address.xml.i>
- #include <include/name-server-ipv4.xml.i>
+ #include <include/name-server-ipv4-ipv6.xml.i>
#include <include/accel-ppp/wins-server.xml.i>
#include <include/accel-ppp/client-ip-pool.xml.i>
<node name="authentication">
@@ -63,30 +63,6 @@
</properties>
<defaultValue>mschap-v2</defaultValue>
</leafNode>
- <leafNode name="mppe">
- <properties>
- <help>Specifies mppe negotioation preference. (default require mppe 128-bit stateless</help>
- <valueHelp>
- <format>deny</format>
- <description>deny mppe</description>
- </valueHelp>
- <valueHelp>
- <format>prefer</format>
- <description>ask client for mppe, if it rejects do not fail</description>
- </valueHelp>
- <valueHelp>
- <format>require</format>
- <description>ask client for mppe, if it rejects drop connection</description>
- </valueHelp>
- <constraint>
- <regex>(deny|prefer|require)</regex>
- </constraint>
- <completionHelp>
- <list>deny prefer require</list>
- </completionHelp>
- </properties>
- <defaultValue>prefer</defaultValue>
- </leafNode>
#include <include/accel-ppp/auth-mode.xml.i>
<node name="local-users">
<properties>
@@ -134,6 +110,9 @@
</children>
</node>
#include <include/accel-ppp/default-pool.xml.i>
+ #include <include/accel-ppp/client-ipv6-pool.xml.i>
+ #include <include/accel-ppp/default-ipv6-pool.xml.i>
+ #include <include/accel-ppp/ppp-options.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/vpn_sstp.xml.in b/interface-definitions/vpn_sstp.xml.in
index a1b69f990..0d5d53301 100644
--- a/interface-definitions/vpn_sstp.xml.in
+++ b/interface-definitions/vpn_sstp.xml.in
@@ -36,18 +36,8 @@
<defaultValue>443</defaultValue>
</leafNode>
#include <include/accel-ppp/default-pool.xml.i>
- <node name="ppp-options">
- <properties>
- <help>PPP (Point-to-Point Protocol) settings</help>
- </properties>
- <children>
- #include <include/accel-ppp/ppp-mppe.xml.i>
- #include <include/accel-ppp/ppp-options-ipv4.xml.i>
- #include <include/accel-ppp/ppp-options-ipv6.xml.i>
- #include <include/accel-ppp/lcp-echo-interval-failure.xml.i>
- #include <include/accel-ppp/lcp-echo-timeout.xml.i>
- </children>
- </node>
+ #include <include/accel-ppp/default-ipv6-pool.xml.i>
+ #include <include/accel-ppp/ppp-options.xml.i>
<node name="ssl">
<properties>
<help>SSL Certificate, SSL Key and CA</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>&lt;ASNUM&gt;</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>&lt;x.x.x.x/x&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ <children>
+ <tagNode name="as-number">
+ <properties>
+ <help>AS Number</help>
+ <completionHelp>
+ <list>&lt;ASNUM&gt;</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>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</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>&lt;h:h:h:h:h:h:h:h&gt; &lt;h:h:h:h:h:h:h:h/x&gt;</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..d60402e48 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'IPv6 delegate-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(
+ "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 0ca41718f..bee85315d 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -29,7 +29,7 @@ There are multiple types of config tree nodes in VyOS, each requires
its own set of operations.
*Leaf nodes* (such as "address" in interfaces) can have values, but cannot
-have children.
+have children.
Leaf nodes can have one value, multiple values, or no values at all.
For example, "system host-name" is a single-value leaf node,
@@ -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.
@@ -258,7 +290,9 @@ class Config(object):
def get_config_dict(self, path=[], effective=False, key_mangling=None,
get_first_key=False, no_multi_convert=False,
no_tag_node_value_mangle=False,
- with_defaults=False, with_recursive_defaults=False):
+ with_defaults=False,
+ with_recursive_defaults=False,
+ with_pki=False):
"""
Args:
path (str list): Configuration tree path, can be empty
@@ -274,6 +308,7 @@ class Config(object):
del kwargs['no_multi_convert']
del kwargs['with_defaults']
del kwargs['with_recursive_defaults']
+ del kwargs['with_pki']
lpath = self._make_path(path)
root_dict = self.get_cached_root_dict(effective)
@@ -298,6 +333,18 @@ class Config(object):
else:
conf_dict = ConfigDict(conf_dict)
+ if with_pki and conf_dict:
+ pki_dict = self.get_config_dict(['pki'], key_mangling=('-', '_'),
+ 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
setattr(conf_dict, '_dict_kwargs', kwargs)
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 6a421485f..4111d7271 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -163,6 +163,9 @@ def node_changed(conf, path, key_mangling=None, recursive=False, expand_nodes=No
output.extend(list(tmp['delete'].keys()))
if expand_nodes & Diff.ADD:
output.extend(list(tmp['add'].keys()))
+
+ # remove duplicate keys from list, this happens when a node (e.g. description) is altered
+ output = list(dict.fromkeys(output))
return output
def get_removed_vlans(conf, path, dict):
@@ -424,7 +427,7 @@ def get_pppoe_interfaces(conf, vrf=None):
return pppoe_interfaces
-def get_interface_dict(config, base, ifname='', recursive_defaults=True):
+def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pki=False):
"""
Common utility function to retrieve and mangle the interfaces configuration
from the CLI input nodes. All interfaces have a common base where value
@@ -456,7 +459,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True):
get_first_key=True,
no_tag_node_value_mangle=True,
with_defaults=True,
- with_recursive_defaults=recursive_defaults)
+ with_recursive_defaults=recursive_defaults,
+ with_pki=with_pki)
# If interface does not request an IPv4 DHCP address there is no need
# to keep the dhcp-options key
@@ -620,7 +624,7 @@ def get_vlan_ids(interface):
return vlan_ids
-def get_accel_dict(config, base, chap_secrets):
+def get_accel_dict(config, base, chap_secrets, with_pki=False):
"""
Common utility function to retrieve and mangle the Accel-PPP configuration
from different CLI input nodes. All Accel-PPP services have a common base
@@ -635,7 +639,8 @@ def get_accel_dict(config, base, chap_secrets):
dict = config.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True,
no_tag_node_value_mangle=True,
- with_recursive_defaults=True)
+ with_recursive_defaults=True,
+ with_pki=with_pki)
# set CPUs cores to process requests
dict.update({'thread_count' : get_half_cpus()})
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 a2622fa00..eee11bd2d 100644
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -226,6 +226,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!=' if exclude else '=='
operator = f'& {address_mask} {operator}'
output.append(f'{ip_name} {prefix}addr {operator} @A{def_suffix}_{group_name}')
+ elif 'dynamic_address_group' in group:
+ group_name = group['dynamic_address_group']
+ operator = ''
+ exclude = group_name[0] == "!"
+ if exclude:
+ operator = '!='
+ group_name = group_name[1:]
+ output.append(f'{ip_name} {prefix}addr {operator} @DA{def_suffix}_{group_name}')
# Generate firewall group domain-group
elif 'domain_group' in group:
group_name = group['domain_group']
@@ -280,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 = '!='
@@ -295,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 = '!='
@@ -419,6 +427,18 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append('counter')
+ if 'add_address_to_group' in rule_conf:
+ for side in ['destination_address', 'source_address']:
+ if side in rule_conf['add_address_to_group']:
+ prefix = side[0]
+ side_conf = rule_conf['add_address_to_group'][side]
+ dyn_group = side_conf['address_group']
+ if 'timeout' in side_conf:
+ timeout_value = side_conf['timeout']
+ output.append(f'set update ip{def_suffix} {prefix}addr timeout {timeout_value} @DA{def_suffix}_{dyn_group}')
+ else:
+ output.append(f'set update ip{def_suffix} saddr @DA{def_suffix}_{dyn_group}')
+
if 'set' in rule_conf:
output.append(parse_policy_set(rule_conf['set'], def_suffix))
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..720bebec3 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 'bootfile_server' in host_config['option']:
+ reservation['next-server'] = host_config['option']['bootfile_server']
- if options:
- out['option-data'] = options
+ 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 range_config:
+ pool['pool'] = range_config['prefix']
- if 'prefix' in addr_range:
- for prefix in addr_range['prefix']:
- pools.append({'pool': prefix})
+ if 'start' in range_config:
+ start = range_config['start']
+ stop = range_config['stop']
+ pool['pool'] = f'{start} - {stop}'
- 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 'option' in range_config:
+ pool['option-data'] = kea6_parse_options(range_config['option'])
+
+ pools.append(pool)
out['pools'] = pools
@@ -218,11 +237,17 @@ def kea6_parse_subnet(subnet, config):
if 'prefix' in config['prefix_delegation']:
for prefix, pd_conf in config['prefix_delegation']['prefix'].items():
- pd_pools.append({
+ pd_pool = {
'prefix': prefix,
'prefix-len': int(pd_conf['prefix_length']),
'delegated-len': int(pd_conf['delegated_length'])
- })
+ }
+
+ if 'excluded_prefix' in pd_conf:
+ pd_pool['excluded-prefix'] = pd_conf['excluded_prefix']
+ pd_pool['excluded-prefix-len'] = int(pd_conf['excluded_prefix_length'])
+
+ pd_pools.append(pd_pool)
out['pd-pools'] = pd_pools
@@ -256,13 +281,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 +318,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..0e6e522b9 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,121 @@ 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)
+
+ def test_accel_ppp_options(self):
+ # Test configuration of local authentication for PPPoE server
+ self.basic_config()
+
+ # other settings
+ mppe = 'require'
+ self.set(['ppp-options', 'disable-ccp'])
+ self.set(['ppp-options', 'mppe', mppe])
+
+ # min-mtu
+ min_mtu = '1400'
+ self.set(['ppp-options', 'min-mtu', min_mtu])
+
+ # mru
+ mru = '9000'
+ self.set(['ppp-options', 'mru', mru])
+
+ # interface-cache
+ interface_cache = '128000'
+ self.set(['ppp-options', 'interface-cache', interface_cache])
+
+ # ipv6
+ allow_ipv6 = 'allow'
+ allow_ipv4 = 'require'
+ random = 'random'
+ lcp_failure = '4'
+ lcp_interval = '40'
+ lcp_timeout = '100'
+ self.set(['ppp-options', 'ipv4', allow_ipv4])
+ self.set(['ppp-options', 'ipv6', allow_ipv6])
+ self.set(['ppp-options', 'ipv6-interface-id', random])
+ self.set(['ppp-options', 'ipv6-accept-peer-interface-id'])
+ self.set(['ppp-options', 'ipv6-peer-interface-id', random])
+ self.set(['ppp-options', 'lcp-echo-failure', lcp_failure])
+ self.set(['ppp-options', 'lcp-echo-interval', lcp_interval])
+ self.set(['ppp-options', 'lcp-echo-timeout', lcp_timeout])
+ # commit changes
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=')
+ conf.read(self._config_file)
+
+ self.assertEqual(conf['chap-secrets']['gw-ip-address'], self._gateway)
+
+ # check ppp
+ self.assertEqual(conf['ppp']['mppe'], mppe)
+ self.assertEqual(conf['ppp']['min-mtu'], min_mtu)
+ self.assertEqual(conf['ppp']['mru'], mru)
+
+ self.assertEqual(conf['ppp']['ccp'],'0')
+
+ # 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'))
+ self.assertEqual(conf['ppp']['lcp-echo-failure'], lcp_failure)
+ self.assertEqual(conf['ppp']['lcp-echo-interval'], lcp_interval)
+ self.assertEqual(conf['ppp']['lcp-echo-timeout'], lcp_timeout) \ No newline at end of file
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_container.py b/smoketest/scripts/cli/test_container.py
index cdf46a6e1..9094e27dd 100755
--- a/smoketest/scripts/cli/test_container.py
+++ b/smoketest/scripts/cli/test_container.py
@@ -188,5 +188,27 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix4).ip + 1))
self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'] , str(ip_interface(prefix4).ip + ii))
+ def test_uid_gid(self):
+ cont_name = 'uid-test'
+ gid = '100'
+ uid = '1001'
+
+ self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
+ self.cli_set(base_path + ['name', cont_name, 'image', cont_image])
+ self.cli_set(base_path + ['name', cont_name, 'gid', gid])
+
+ # verify() - GID can only be set if UID is set
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['name', cont_name, 'uid', uid])
+
+ self.cli_commit()
+
+ # verify
+ tmp = cmd(f'sudo podman exec -it {cont_name} id -u')
+ self.assertEqual(tmp, uid)
+ tmp = cmd(f'sudo podman exec -it {cont_name} id -g')
+ self.assertEqual(tmp, gid)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 2be616da1..a7dd11145 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -403,6 +403,46 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip vyos_filter')
+ def test_ipv4_dynamic_groups(self):
+ group01 = 'knock01'
+ group02 = 'allowed'
+
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group01])
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'address-group', group02])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
+
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'DA_{group01}'],
+ [f'DA_{group02}'],
+ ['type ipv4_addr'],
+ ['flags dynamic,timeout'],
+ ['chain VYOS_INPUT_filter {'],
+ ['type filter hook input priority filter', 'policy accept'],
+ ['tcp dport 5151', f'update @DA_{group01}', '{ ip saddr timeout 30s }', 'drop'],
+ ['tcp dport 7272', f'ip saddr @DA_{group01}', f'update @DA_{group02}', '{ ip saddr timeout 5m }', 'drop'],
+ ['tcp dport 22', f'ip saddr @DA_{group02}', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
def test_ipv6_basic_rules(self):
name = 'v6-smoketest'
@@ -540,6 +580,47 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+ def test_ipv6_dynamic_groups(self):
+ group01 = 'knock01'
+ group02 = 'allowed'
+
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group01])
+ self.cli_set(['firewall', 'group', 'dynamic-group', 'ipv6-address-group', group02])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'port', '5151'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'address-group', group01])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'add-address-to-group', 'source-address', 'timeout', '30s'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'destination', 'port', '7272'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'source', 'group', 'dynamic-address-group', group01])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'address-group', group02])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '20', 'add-address-to-group', 'source-address', 'timeout', '5m'])
+
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'action', 'accept'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'destination', 'port', '22'])
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '30', 'source', 'group', 'dynamic-address-group', group02])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'DA6_{group01}'],
+ [f'DA6_{group02}'],
+ ['type ipv6_addr'],
+ ['flags dynamic,timeout'],
+ ['chain VYOS_IPV6_INPUT_filter {'],
+ ['type filter hook input priority filter', 'policy accept'],
+ ['tcp dport 5151', f'update @DA6_{group01}', '{ ip6 saddr timeout 30s }', 'drop'],
+ ['tcp dport 7272', f'ip6 saddr @DA6_{group01}', f'update @DA6_{group02}', '{ ip6 saddr timeout 5m }', 'drop'],
+ ['tcp dport 22', f'ip6 saddr @DA6_{group02}', 'accept']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
def test_ipv4_state_and_status_rules(self):
name = 'smoketest-state'
interface = 'eth0'
@@ -671,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'])
@@ -704,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..5a831b8a0 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))
@@ -213,12 +215,16 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
delegate_start = '2001:db8:ee::'
delegate_len = '64'
prefix_len = '56'
+ exclude_len = '66'
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])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix', delegate_start])
+ self.cli_set(pool + ['prefix-delegation', 'prefix', delegate_start, 'excluded-prefix-length', exclude_len])
# commit changes
self.cli_commit()
@@ -238,7 +244,13 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'pd-pools'],
- {'prefix': delegate_start, 'prefix-len': int(prefix_len), 'delegated-len': int(delegate_len)})
+ {
+ 'prefix': delegate_start,
+ 'prefix-len': int(prefix_len),
+ 'delegated-len': int(delegate_len),
+ 'excluded-prefix': delegate_start,
+ 'excluded-prefix-len': int(exclude_len)
+ })
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
@@ -250,7 +262,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 +272,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 703e3e8c4..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}'}
@@ -395,27 +412,22 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
@ignore_warning(InsecureRequestWarning)
def test_api_config_file_load_http(self):
- """Test load config from HTTP URL
- """
+ # Test load config from HTTP URL
address = '127.0.0.1'
key = 'VyOS-key'
url = f'https://{address}/config-file'
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 = {
@@ -442,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..20a168b58 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,49 @@ 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)
+
+ @unittest.skip("PPP is not a part of IPoE")
+ def test_accel_ppp_options(self):
+ pass
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..d7c7aa164 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
@@ -59,9 +59,6 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase):
self.assertTrue(conf['ppp'].getboolean('verbose'))
self.assertTrue(conf['ppp'].getboolean('check-ip'))
self.assertEqual(conf['ppp']['mtu'], mtu)
- self.assertEqual(conf['ppp']['lcp-echo-interval'], '30')
- self.assertEqual(conf['ppp']['lcp-echo-timeout'], '0')
- self.assertEqual(conf['ppp']['lcp-echo-failure'], '3')
super().verify(conf)
@@ -70,54 +67,14 @@ class TestServicePPPoEServer(BasicAccelPPPTest.TestCase):
self.set(['access-concentrator', ac_name])
self.set(['interface', interface])
-
- def test_pppoe_server_ppp_options(self):
- # Test configuration of local authentication for PPPoE server
+ def test_pppoe_limits(self):
self.basic_config()
-
- # other settings
- mppe = 'require'
- self.set(['ppp-options', 'ccp'])
- self.set(['ppp-options', 'mppe', mppe])
self.set(['limits', 'connection-limit', '20/min'])
-
- # min-mtu
- min_mtu = '1400'
- self.set(['ppp-options', 'min-mtu', min_mtu])
-
- # mru
- mru = '9000'
- self.set(['ppp-options', 'mru', mru])
-
- # interface-cache
- interface_cache = '128000'
- self.set(['ppp-options', 'interface-cache', interface_cache])
-
- # commit changes
self.cli_commit()
-
- # Validate configuration values
conf = ConfigParser(allow_no_value=True, delimiters='=')
conf.read(self._config_file)
-
- # basic verification
- self.verify(conf)
-
- self.assertEqual(conf['chap-secrets']['gw-ip-address'], self._gateway)
-
- # check ppp
- self.assertEqual(conf['ppp']['mppe'], mppe)
- self.assertEqual(conf['ppp']['min-mtu'], min_mtu)
- self.assertEqual(conf['ppp']['mru'], mru)
-
- self.assertTrue(conf['ppp'].getboolean('ccp'))
-
- # check other settings
self.assertEqual(conf['connlimit']['limit'], '20/min')
- # check interface-cache
- self.assertEqual(conf['ppp']['unit-cache'], interface_cache)
-
def test_pppoe_server_authentication_protocols(self):
# Test configuration of local authentication for PPPoE server
self.basic_config()
@@ -154,45 +111,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..3d9d94f52 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
@@ -38,44 +38,6 @@ class TestVPNL2TPServer(BasicAccelPPPTest.TestCase):
def basic_protocol_specific_config(self):
pass
- def test_l2tp_server_ppp_options(self):
- # Test configuration of local authentication for PPPoE server
- self.basic_config()
- mtu = '1425'
- lcp_echo_failure = '5'
- lcp_echo_interval = '40'
- lcp_echo_timeout = '3000'
- # other settings
- mppe = 'require'
- self.set(['ccp-disable'])
- self.set(['ppp-options', 'mppe', mppe])
- self.set(['authentication', 'radius', 'preallocate-vif'])
- self.set(['mtu', mtu])
- self.set(['ppp-options', 'lcp-echo-failure', lcp_echo_failure])
- self.set(['ppp-options', 'lcp-echo-interval', lcp_echo_interval])
- self.set(['ppp-options', 'lcp-echo-timeout', lcp_echo_timeout])
-
- # commit changes
- self.cli_commit()
-
- # Validate configuration values
- conf = ConfigParser(allow_no_value=True, delimiters='=')
- conf.read(self._config_file)
-
- # basic verification
- self.verify(conf)
-
- # check ppp
- self.assertEqual(conf['ppp']['mppe'], mppe)
- self.assertFalse(conf['ppp'].getboolean('ccp'))
- self.assertEqual(conf['ppp']['unit-preallocate'], '1')
- self.assertTrue(conf['ppp'].getboolean('verbose'))
- self.assertTrue(conf['ppp'].getboolean('check-ip'))
- self.assertEqual(conf['ppp']['mtu'], mtu)
- self.assertEqual(conf['ppp']['lcp-echo-interval'], lcp_echo_interval)
- self.assertEqual(conf['ppp']['lcp-echo-timeout'], lcp_echo_timeout)
- self.assertEqual(conf['ppp']['lcp-echo-failure'], lcp_echo_failure)
-
def test_l2tp_server_authentication_protocols(self):
# Test configuration of local authentication for PPPoE server
self.basic_config()
@@ -92,44 +54,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..40dcb7f80 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
@@ -40,25 +40,6 @@ class TestVPNPPTPServer(BasicAccelPPPTest.TestCase):
def basic_protocol_specific_config(self):
pass
- def test_accel_name_servers(self):
- # Verify proper Name-Server configuration for IPv4
- self.basic_config()
-
- nameserver = ["192.0.2.1", "192.0.2.2"]
- for ns in nameserver:
- self.set(["name-server", ns])
-
- # commit changes
- self.cli_commit()
-
- # Validate configuration values
- conf = ConfigParser(allow_no_value=True, delimiters="=", strict=False)
- conf.read(self._config_file)
-
- # IPv4 and IPv6 nameservers must be checked individually
- for ns in nameserver:
- self.assertIn(ns, [conf["dns"]["dns1"], conf["dns"]["dns2"]])
-
def test_accel_local_authentication(self):
# Test configuration of local authentication
self.basic_config()
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/container.py b/src/conf_mode/container.py
index 59d11c5a3..321d00abf 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -214,6 +214,10 @@ def verify(container):
if {'allow_host_networks', 'network'} <= set(container_config):
raise ConfigError(f'"allow-host-networks" and "network" for "{name}" cannot be both configured at the same time!')
+ # gid cannot be set without uid
+ if 'gid' in container_config and 'uid' not in container_config:
+ raise ConfigError(f'Cannot set "gid" without "uid" for container')
+
# Add new network
if 'network' in container:
for network, network_config in container['network'].items():
@@ -308,6 +312,14 @@ def generate_run_arguments(name, container_config):
# If listen_addresses is empty, just include the standard publish command
port += f' --publish {sport}:{dport}/{protocol}'
+ # Set uid and gid
+ uid = ''
+ if 'uid' in container_config:
+ uid = container_config['uid']
+ if 'gid' in container_config:
+ uid += ':' + container_config['gid']
+ uid = f'--user {uid}'
+
# Bind volume
volume = ''
if 'volume' in container_config:
@@ -320,7 +332,7 @@ def generate_run_arguments(name, container_config):
container_base_cmd = f'--detach --interactive --tty --replace {cap_add} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label}'
+ f'--name {name} {hostname} {device} {port} {volume} {env_opt} {label} {uid}'
entrypoint = ''
if 'entrypoint' in container_config:
diff --git a/src/conf_mode/interfaces_ethernet.py b/src/conf_mode/interfaces_ethernet.py
index 7374a29f7..2c0f846c3 100755
--- a/src/conf_mode/interfaces_ethernet.py
+++ b/src/conf_mode/interfaces_ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2021 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
@@ -150,19 +150,12 @@ def get_config(config=None):
else:
conf = Config()
- # This must be called prior to get_interface_dict(), as this function will
- # alter the config level (config.set_level())
- pki = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
base = ['interfaces', 'ethernet']
- ifname, ethernet = get_interface_dict(conf, base)
+ ifname, ethernet = get_interface_dict(conf, base, with_pki=True)
+
if 'is_bond_member' in ethernet:
update_bond_options(conf, ethernet)
- if 'deleted' not in ethernet:
- if pki: ethernet['pki'] = pki
-
tmp = is_node_changed(conf, base + [ifname, 'speed'])
if tmp: ethernet.update({'speed_duplex_changed': {}})
@@ -171,8 +164,6 @@ def get_config(config=None):
return ethernet
-
-
def verify_speed_duplex(ethernet: dict, ethtool: Ethtool):
"""
Verify speed and duplex
diff --git a/src/conf_mode/interfaces_openvpn.py b/src/conf_mode/interfaces_openvpn.py
index bdeb44837..45569dd21 100755
--- a/src/conf_mode/interfaces_openvpn.py
+++ b/src/conf_mode/interfaces_openvpn.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
@@ -89,16 +89,12 @@ def get_config(config=None):
conf = Config()
base = ['interfaces', 'openvpn']
- ifname, openvpn = get_interface_dict(conf, base)
+ ifname, openvpn = get_interface_dict(conf, base, with_pki=True)
openvpn['auth_user_pass_file'] = '/run/openvpn/{ifname}.pw'.format(**openvpn)
if 'deleted' in openvpn:
return openvpn
- openvpn['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
-
if is_node_changed(conf, base + [ifname, 'openvpn-option']):
openvpn.update({'restart_required': {}})
if is_node_changed(conf, base + [ifname, 'enable-dco']):
@@ -167,9 +163,10 @@ def verify_pki(openvpn):
raise ConfigError(f'Invalid shared-secret on openvpn interface {interface}')
# If PSK settings are correct, warn about its deprecation
- DeprecationWarning("OpenVPN shared-secret support will be removed in future VyOS versions.\n\
- Please migrate your site-to-site tunnels to TLS.\n\
- You can use self-signed certificates with peer fingerprint verification, consult the documentation for details.")
+ DeprecationWarning('OpenVPN shared-secret support will be removed in future '\
+ 'VyOS versions. Please migrate your site-to-site tunnels to '\
+ 'TLS. You can use self-signed certificates with peer fingerprint '\
+ 'verification, consult the documentation for details.')
if tls:
if (mode in ['server', 'client']) and ('ca_certificate' not in tls):
@@ -729,4 +726,3 @@ if __name__ == '__main__':
except ConfigError as e:
print(e)
exit(1)
-
diff --git a/src/conf_mode/interfaces_sstpc.py b/src/conf_mode/interfaces_sstpc.py
index b588910dc..b9d7a74fb 100755
--- a/src/conf_mode/interfaces_sstpc.py
+++ b/src/conf_mode/interfaces_sstpc.py
@@ -45,7 +45,7 @@ def get_config(config=None):
else:
conf = Config()
base = ['interfaces', 'sstpc']
- ifname, sstpc = get_interface_dict(conf, base)
+ ifname, sstpc = get_interface_dict(conf, base, with_pki=True)
# We should only terminate the SSTP client session if critical parameters
# change. All parameters that can be changed on-the-fly (like interface
@@ -57,10 +57,6 @@ def get_config(config=None):
# bail out early - no need to further process other nodes
break
- # Load PKI certificates for later processing
- sstpc['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
return sstpc
def verify(sstpc):
diff --git a/src/conf_mode/load-balancing_reverse-proxy.py b/src/conf_mode/load-balancing_reverse-proxy.py
index 333ebc66c..7338fe573 100755
--- a/src/conf_mode/load-balancing_reverse-proxy.py
+++ b/src/conf_mode/load-balancing_reverse-proxy.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
@@ -43,17 +43,14 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'reverse-proxy']
+ if not conf.exists(base):
+ return None
lb = conf.get_config_dict(base,
get_first_key=True,
key_mangling=('-', '_'),
- no_tag_node_value_mangle=True)
-
- if lb:
- lb['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True, no_tag_node_value_mangle=True)
-
- if lb:
- lb = conf.merge_defaults(lb, recursive=True)
+ no_tag_node_value_mangle=True,
+ with_recursive_defaults=True,
+ with_pki=True)
return lb
diff --git a/src/conf_mode/nat.py b/src/conf_mode/nat.py
index 20570da62..26822b755 100755
--- a/src/conf_mode/nat.py
+++ b/src/conf_mode/nat.py
@@ -69,6 +69,10 @@ def get_config(config=None):
nat['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
+ # Remove dynamic firewall groups if present:
+ if 'dynamic_group' in nat['firewall_group']:
+ del nat['firewall_group']['dynamic_group']
+
return nat
def verify_rule(config, err_msg, groups_dict):
@@ -83,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/policy_route.py b/src/conf_mode/policy_route.py
index adad012de..6d7a06714 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -53,6 +53,10 @@ def get_config(config=None):
policy['firewall_group'] = conf.get_config_dict(['firewall', 'group'], key_mangling=('-', '_'), get_first_key=True,
no_tag_node_value_mangle=True)
+ # Remove dynamic firewall groups if present:
+ if 'dynamic_group' in policy['firewall_group']:
+ del policy['firewall_group']['dynamic_group']
+
return policy
def verify_rule(policy, name, rule_conf, ipv6, rule_id):
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..add83eb0d 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:
@@ -129,6 +144,23 @@ def verify(dhcpv6):
if prefix_config['prefix_length'] > prefix_config['delegated_length']:
raise ConfigError('Length of delegated IPv6 prefix must be within parent prefix')
+ if 'excluded_prefix' in prefix_config:
+ if 'excluded_prefix_length' not in prefix_config:
+ raise ConfigError('Length of excluded IPv6 prefix must be configured')
+
+ prefix_len = prefix_config['prefix_length']
+ prefix_obj = ip_network(f'{prefix}/{prefix_len}')
+
+ excluded_prefix = prefix_config['excluded_prefix']
+ excluded_len = prefix_config['excluded_prefix_length']
+ excluded_obj = ip_network(f'{excluded_prefix}/{excluded_len}')
+
+ if excluded_len <= prefix_config['delegated_length']:
+ raise ConfigError('Excluded IPv6 prefix must be smaller than delegated prefix')
+
+ if not excluded_obj.subnet_of(prefix_obj):
+ raise ConfigError(f'Excluded prefix "{excluded_prefix}" does not exist in the prefix')
+
# Static mappings don't require anything (but check if IP is in subnet if it's set)
if 'static_mapping' in subnet_config:
for mapping, mapping_config in subnet_config['static_mapping'].items():
@@ -142,13 +174,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 3dc5dfc01..46efc3c93 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/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
@@ -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,98 +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)
-
- if https:
- https['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True)
+ https = conf.get_config_dict(base, get_first_key=True,
+ key_mangling=('-', '_'),
+ with_pki=True)
- https['children_changed'] = diff.node_changed_children(base)
- https['api_add_or_delete'] = diff.node_changed_presence(base + ['api'])
+ # store path to API config file for later use in templates
+ https['api_config_state'] = api_config_state
+ # get fully qualified system hsotname
+ https['hostname'] = socket.getfqdn()
- if 'api' not in https:
- return https
-
- 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 'certificate' in certificates:
- if not https['pki']:
- raise ConfigError("PKI is not configured")
+ 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!')
- cert_name = certificates['certificate']
+ if cert_name not in https['pki']['certificate']:
+ raise ConfigError('Invalid certificate in configuration!')
- 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)
@@ -192,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
@@ -302,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 9e9385ddb..d074ed159 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.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
@@ -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'
@@ -87,15 +89,13 @@ def get_config(config=None):
ipsec = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
- with_recursive_defaults=True)
+ with_recursive_defaults=True,
+ with_pki=True)
ipsec['dhcp_no_address'] = {}
ipsec['install_routes'] = 'no' if conf.exists(base + ["options", "disable-route-autoinstall"]) else default_install_routes
ipsec['interface_change'] = leaf_node_changed(conf, base + ['interface'])
ipsec['nhrp_exists'] = conf.exists(['protocols', 'nhrp', 'tunnel'])
- ipsec['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True)
tmp = conf.get_config_dict(l2tp_base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
@@ -160,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']:
@@ -396,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..36b3d2a30 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
@@ -51,11 +51,6 @@ def get_config(config=None):
# Multiple named pools require ordered values T5099
l2tp['ordered_named_pools'] = get_pools_in_order(
dict_search('client_ip_pool', l2tp))
- l2tp['ip6_column'] = []
- if dict_search('client_ipv6_pool.prefix', l2tp):
- l2tp['ip6_column'].append('ipv6')
- if dict_search('client_ipv6_pool.delegate', l2tp):
- l2tp['ip6_column'].append('ip6-db')
l2tp['server_type'] = 'l2tp'
return l2tp
@@ -70,15 +65,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_openconnect.py b/src/conf_mode/vpn_openconnect.py
index a039172c4..421ac6997 100755
--- a/src/conf_mode/vpn_openconnect.py
+++ b/src/conf_mode/vpn_openconnect.py
@@ -56,12 +56,8 @@ def get_config(config=None):
ocserv = conf.get_config_dict(base, key_mangling=('-', '_'),
get_first_key=True,
- with_recursive_defaults=True)
-
- if ocserv:
- ocserv['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- no_tag_node_value_mangle=True,
- get_first_key=True)
+ with_recursive_defaults=True,
+ with_pki=True)
return ocserv
diff --git a/src/conf_mode/vpn_pptp.py b/src/conf_mode/vpn_pptp.py
index f769be39f..b1d5067d5 100755
--- a/src/conf_mode/vpn_pptp.py
+++ b/src/conf_mode/vpn_pptp.py
@@ -22,6 +22,7 @@ from vyos.config import Config
from vyos.template import render
from vyos.utils.process import call
from vyos.utils.dict import dict_search
+from vyos.accel_ppp_util import verify_accel_ppp_base_service
from vyos.accel_ppp_util import verify_accel_ppp_ip_pool
from vyos.accel_ppp_util import get_pools_in_order
from vyos import ConfigError
@@ -58,42 +59,10 @@ def get_config(config=None):
def verify(pptp):
if not pptp:
return None
- auth_mode = dict_search('authentication.mode', pptp)
- if auth_mode == 'local':
- if not dict_search('authentication.local_users', pptp):
- raise ConfigError(
- 'PPTP local auth mode requires local users to be configured!')
-
- for user in dict_search('authentication.local_users.username', pptp):
- user_config = pptp['authentication']['local_users']['username'][
- user]
- if 'password' not in user_config:
- raise ConfigError(f'Password required for local user "{user}"')
-
- elif auth_mode == 'radius':
- if not dict_search('authentication.radius.server', pptp):
- raise ConfigError(
- 'RADIUS authentication requires at least one server')
- for server in dict_search('authentication.radius.server', pptp):
- radius_config = pptp['authentication']['radius']['server'][server]
- if 'key' not in radius_config:
- 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_base_service(pptp)
verify_accel_ppp_ip_pool(pptp)
- if 'name_server' in pptp:
- if len(pptp['name_server']) > 2:
- raise ConfigError(
- 'Not more then two IPv4 DNS name-servers can be configured'
- )
-
if 'wins_server' in pptp and len(pptp['wins_server']) > 2:
raise ConfigError(
'Not more then two WINS name-servers can be configured')
@@ -111,6 +80,7 @@ def generate(pptp):
return None
+
def apply(pptp):
if not pptp:
call('systemctl stop accel-ppp@pptp.service')
diff --git a/src/conf_mode/vpn_sstp.py b/src/conf_mode/vpn_sstp.py
index ac053cc76..5c229fe62 100755
--- a/src/conf_mode/vpn_sstp.py
+++ b/src/conf_mode/vpn_sstp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-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
@@ -20,7 +20,6 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_accel_dict
-from vyos.configdict import dict_merge
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
from vyos.template import render
@@ -54,14 +53,11 @@ def get_config(config=None):
return None
# retrieve common dictionary keys
- sstp = get_accel_dict(conf, base, sstp_chap_secrets)
+ sstp = get_accel_dict(conf, base, sstp_chap_secrets, with_pki=True)
if dict_search('client_ip_pool', sstp):
# Multiple named pools require ordered values T5099
sstp['ordered_named_pools'] = get_pools_in_order(dict_search('client_ip_pool', sstp))
- if sstp:
- sstp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'),
- get_first_key=True,
- no_tag_node_value_mangle=True)
+
sstp['server_type'] = 'sstp'
return sstp
@@ -77,11 +73,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/sudoers.d/vyos b/src/etc/sudoers.d/vyos
index c099446ba..63a944f41 100644
--- a/src/etc/sudoers.d/vyos
+++ b/src/etc/sudoers.d/vyos
@@ -44,6 +44,8 @@ Cmnd_Alias DIAGNOSTICS = /bin/ip vrf exec * /bin/ping *, \
/bin/ip vrf exec * /bin/traceroute *, \
/bin/ip vrf exec * /usr/bin/mtr *, \
/usr/libexec/vyos/op_mode/*
+Cmnd_Alias KEA_IP6_ROUTES = /sbin/ip -6 route replace *,\
+ /sbin/ip -6 route del *
%operator ALL=NOPASSWD: DATE, IPTABLES, ETHTOOL, IPFLUSH, HWINFO, \
PPPOE_CMDS, PCAPTURE, /usr/sbin/wanpipemon, \
DMIDECODE, DISK, CONNTRACK, IP6TABLES, \
@@ -55,3 +57,4 @@ Cmnd_Alias DIAGNOSTICS = /bin/ip vrf exec * /bin/ping *, \
# Allow members of group sudo to execute any command
%sudo ALL=NOPASSWD: ALL
+_kea ALL=NOPASSWD: KEA_IP6_ROUTES
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/l2tp/7-to-8 b/src/migration-scripts/l2tp/7-to-8
new file mode 100755
index 000000000..4956e1155
--- /dev/null
+++ b/src/migration-scripts/l2tp/7-to-8
@@ -0,0 +1,68 @@
+#!/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/>.
+
+# Migrate from 'ccp-disable' to 'ppp-options.disable-ccp'
+# Migration ipv6 options
+
+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']
+if not config.exists(base):
+ exit(0)
+
+#CCP migration
+if config.exists(base + ['ccp-disable']):
+ config.delete(base + ['ccp-disable'])
+ config.set(base + ['ppp-options', 'disable-ccp'])
+
+#IPV6 options migrations
+if config.exists(base + ['ppp-options','ipv6-peer-intf-id']):
+ intf_peer_id = config.return_value(base + ['ppp-options','ipv6-peer-intf-id'])
+ if intf_peer_id == 'ipv4':
+ intf_peer_id = 'ipv4-addr'
+ config.set(base + ['ppp-options','ipv6-peer-interface-id'], value=intf_peer_id, replace=True)
+ config.delete(base + ['ppp-options','ipv6-peer-intf-id'])
+
+if config.exists(base + ['ppp-options','ipv6-intf-id']):
+ intf_id = config.return_value(base + ['ppp-options','ipv6-intf-id'])
+ config.set(base + ['ppp-options','ipv6-interface-id'], value=intf_id, replace=True)
+ config.delete(base + ['ppp-options','ipv6-intf-id'])
+
+if config.exists(base + ['ppp-options','ipv6-accept-peer-intf-id']):
+ config.set(base + ['ppp-options','ipv6-accept-peer-interface-id'])
+ config.delete(base + ['ppp-options','ipv6-accept-peer-intf-id'])
+
+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/pppoe-server/8-to-9 b/src/migration-scripts/pppoe-server/8-to-9
new file mode 100755
index 000000000..ad75c28a1
--- /dev/null
+++ b/src/migration-scripts/pppoe-server/8-to-9
@@ -0,0 +1,69 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2024 VyOS maintainers and contributors
+#
+# This program is free software; you can redistribute it and/or modify
+# it under the terms of the GNU General Public License version 2 or later as
+# published by the Free Software Foundation.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# Change from 'ccp' to 'disable-ccp' in ppp-option section
+# Migration ipv6 options
+
+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']
+if not config.exists(base):
+ exit(0)
+
+#CCP migration
+if config.exists(base + ['ppp-options', 'ccp']):
+ config.delete(base + ['ppp-options', 'ccp'])
+else:
+ config.set(base + ['ppp-options', 'disable-ccp'])
+
+#IPV6 options migrations
+if config.exists(base + ['ppp-options','ipv6-peer-intf-id']):
+ intf_peer_id = config.return_value(base + ['ppp-options','ipv6-peer-intf-id'])
+ if intf_peer_id == 'ipv4':
+ intf_peer_id = 'ipv4-addr'
+ config.set(base + ['ppp-options','ipv6-peer-interface-id'], value=intf_peer_id, replace=True)
+ config.delete(base + ['ppp-options','ipv6-peer-intf-id'])
+
+if config.exists(base + ['ppp-options','ipv6-intf-id']):
+ intf_id = config.return_value(base + ['ppp-options','ipv6-intf-id'])
+ config.set(base + ['ppp-options','ipv6-interface-id'], value=intf_id, replace=True)
+ config.delete(base + ['ppp-options','ipv6-intf-id'])
+
+if config.exists(base + ['ppp-options','ipv6-accept-peer-intf-id']):
+ config.set(base + ['ppp-options','ipv6-accept-peer-interface-id'])
+ config.delete(base + ['ppp-options','ipv6-accept-peer-intf-id'])
+
+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/pptp/3-to-4 b/src/migration-scripts/pptp/3-to-4
new file mode 100755
index 000000000..0a8dad2f4
--- /dev/null
+++ b/src/migration-scripts/pptp/3-to-4
@@ -0,0 +1,51 @@
+#!/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/>.
+
+# - Move 'mppe' from 'authentication' node to 'ppp-options'
+
+import os
+
+from sys import argv
+from sys import exit
+from vyos.configtree import ConfigTree
+
+
+if len(argv) < 2:
+ print("Must specify file name!")
+ exit(1)
+
+file_name = argv[1]
+
+with open(file_name, 'r') as f:
+ config_file = f.read()
+
+config = ConfigTree(config_file)
+base = ['vpn', 'pptp', 'remote-access']
+
+if not config.exists(base):
+ exit(0)
+
+if config.exists(base + ['authentication','mppe']):
+ mppe = config.return_value(base + ['authentication','mppe'])
+ config.set(base + ['ppp-options', 'mppe'], value=mppe, replace=True)
+ config.delete(base + ['authentication','mppe'])
+
+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/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/firewall.py b/src/op_mode/firewall.py
index 36bb013fe..4dcffc412 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -327,6 +327,8 @@ def show_firewall_group(name=None):
dest_group = dict_search_args(rule_conf, 'destination', 'group', group_type)
in_interface = dict_search_args(rule_conf, 'inbound_interface', 'group')
out_interface = dict_search_args(rule_conf, 'outbound_interface', 'group')
+ dyn_group_source = dict_search_args(rule_conf, 'add_address_to_group', 'source_address', group_type)
+ dyn_group_dst = dict_search_args(rule_conf, 'add_address_to_group', 'destination_address', group_type)
if source_group:
if source_group[0] == "!":
source_group = source_group[1:]
@@ -348,6 +350,14 @@ def show_firewall_group(name=None):
if group_name == out_interface:
out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+ if dyn_group_source:
+ if group_name == dyn_group_source:
+ out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+ if dyn_group_dst:
+ if group_name == dyn_group_dst:
+ out.append(f'{item}-{name_type}-{priority}-{rule_id}')
+
+
# Look references in route | route6
for name_type in ['route', 'route6']:
if name_type not in policy:
@@ -423,26 +433,37 @@ def show_firewall_group(name=None):
rows = []
for group_type, group_type_conf in firewall['group'].items():
- for group_name, group_conf in group_type_conf.items():
- if name and name != group_name:
- continue
+ ##
+ if group_type != 'dynamic_group':
- references = find_references(group_type, group_name)
- row = [group_name, group_type, '\n'.join(references) or 'N/D']
- if 'address' in group_conf:
- row.append("\n".join(sorted(group_conf['address'])))
- elif 'network' in group_conf:
- row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
- elif 'mac_address' in group_conf:
- row.append("\n".join(sorted(group_conf['mac_address'])))
- elif 'port' in group_conf:
- row.append("\n".join(sorted(group_conf['port'])))
- elif 'interface' in group_conf:
- row.append("\n".join(sorted(group_conf['interface'])))
- else:
- row.append('N/D')
- rows.append(row)
+ for group_name, group_conf in group_type_conf.items():
+ if name and name != group_name:
+ continue
+ references = find_references(group_type, group_name)
+ row = [group_name, group_type, '\n'.join(references) or 'N/D']
+ if 'address' in group_conf:
+ row.append("\n".join(sorted(group_conf['address'])))
+ elif 'network' in group_conf:
+ row.append("\n".join(sorted(group_conf['network'], key=ipaddress.ip_network)))
+ elif 'mac_address' in group_conf:
+ row.append("\n".join(sorted(group_conf['mac_address'])))
+ elif 'port' in group_conf:
+ row.append("\n".join(sorted(group_conf['port'])))
+ elif 'interface' in group_conf:
+ row.append("\n".join(sorted(group_conf['interface'])))
+ else:
+ row.append('N/D')
+ rows.append(row)
+
+ else:
+ for dynamic_type in ['address_group', 'ipv6_address_group']:
+ if dynamic_type in firewall['group']['dynamic_group']:
+ for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items():
+ references = find_references(dynamic_type, dynamic_name)
+ row = [dynamic_name, dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D']
+ row.append('N/D')
+ rows.append(row)
if rows:
print('Firewall Groups\n')
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..47c276270 100755
--- a/src/system/on-dhcp-event.sh
+++ b/src/system/on-dhcp-event.sh
@@ -1,12 +1,20 @@
#!/bin/bash
-
-# This script came from ubnt.com forum user "bradd" in the following post
-# http://community.ubnt.com/t5/EdgeMAX/Automatic-DNS-resolution-of-DHCP-client-names/td-p/651311
-# It has been modified by Ubiquiti to update the /etc/host file
-# instead of adding to the CLI.
-# Thanks to forum user "itsmarcos" for bug fix & improvements
-# Thanks to forum user "ruudboon" for multiple domain fix
-# Thanks to forum user "chibby85" for expire patch and static-mapping
+#
+# 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/>.
+#
+#
if [ $# -lt 1 ]; then
echo Invalid args
@@ -15,28 +23,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/system/on-dhcpv6-event.sh b/src/system/on-dhcpv6-event.sh
new file mode 100755
index 000000000..cbb370999
--- /dev/null
+++ b/src/system/on-dhcpv6-event.sh
@@ -0,0 +1,87 @@
+#!/bin/bash
+#
+# 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/>.
+#
+#
+
+if [ $# -lt 1 ]; then
+ echo Invalid args
+ logger -s -t on-dhcpv6-event "Invalid args \"$@\""
+ exit 1
+fi
+
+action=$1
+
+case "$action" in
+ lease6_renew|lease6_recover)
+ exit 0
+ ;;
+
+ lease6_release|lease6_expire|lease6_decline)
+ ifname=$QUERY6_IFACE_NAME
+ lease_addr=$LEASE6_ADDRESS
+ lease_prefix_len=$LEASE6_PREFIX_LEN
+
+ if [[ "$LEASE6_TYPE" != "IA_PD" ]]; then
+ exit 0
+ fi
+
+ logger -s -t on-dhcpv6-event "Processing route deletion for ${lease_addr}/${lease_prefix_len}"
+ route_cmd="sudo -n /sbin/ip -6 route del ${lease_addr}/${lease_prefix_len}"
+
+ # the ifname is not always present, like in LEASE6_VALID_LIFETIME=0 updates,
+ # but 'route del' works either way. Use interface only if there is one.
+ if [[ "$ifname" != "" ]]; then
+ route_cmd+=" dev ${ifname}"
+ fi
+ route_cmd+=" proto static"
+ eval "$route_cmd"
+
+ exit 0
+ ;;
+
+ leases6_committed)
+ for ((i = 0; i < $LEASES6_SIZE; i++)); do
+ ifname=$QUERY6_IFACE_NAME
+ requester_link_local=$QUERY6_REMOTE_ADDR
+ lease_type_var="LEASES6_AT${i}_TYPE"
+ lease_ip_var="LEASES6_AT${i}_ADDRESS"
+ lease_prefix_len_var="LEASES6_AT${i}_PREFIX_LEN"
+
+ lease_type=${!lease_type_var}
+
+ if [[ "$lease_type" != "IA_PD" ]]; then
+ continue
+ fi
+
+ lease_ip=${!lease_ip_var}
+ lease_prefix_len=${!lease_prefix_len_var}
+
+ logger -s -t on-dhcpv6-event "Processing PD route for ${lease_addr}/${lease_prefix_len}. Link local: ${requester_link_local} ifname: ${ifname}"
+
+ sudo -n /sbin/ip -6 route replace ${lease_ip}/${lease_prefix_len} \
+ via ${requester_link_local} \
+ dev ${ifname} \
+ proto static
+ done
+
+ exit 0
+ ;;
+
+ *)
+ logger -s -t on-dhcpv6-event "Invalid command \"$1\""
+ exit 1
+ ;;
+esac
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