summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/workflows/mirror-pr-and-sync.yml5
-rw-r--r--.github/workflows/package-smoketest.yml3
-rw-r--r--.github/workflows/trigger-pr-mirror-repo-sync.yml5
-rw-r--r--.gitignore5
-rw-r--r--.gitmodules4
-rw-r--r--CODEOWNERS2
-rw-r--r--Makefile20
-rw-r--r--data/config-mode-dependencies/vyos-1x.json3
-rw-r--r--data/config.boot.default2
-rw-r--r--data/op-mode-standardized.json2
-rw-r--r--data/templates/accel-ppp/chap-secrets.ipoe.j22
-rw-r--r--data/templates/conserver/dropbear@.service.j22
-rw-r--r--data/templates/container/containers.conf.j24
-rw-r--r--data/templates/container/registries.conf.j210
-rw-r--r--data/templates/dhcp-client/ipv6.override.conf.j23
-rw-r--r--data/templates/dhcp-server/kea-ctrl-agent.conf.j214
-rw-r--r--data/templates/dhcp-server/kea-dhcp-ddns.conf.j230
-rw-r--r--data/templates/dhcp-server/kea-dhcp4.conf.j237
-rw-r--r--data/templates/firewall/nftables-defines.j218
-rw-r--r--data/templates/firewall/nftables-geoip-update.j233
-rw-r--r--data/templates/firewall/nftables-policy.j217
-rwxr-xr-xdata/templates/firewall/nftables.j242
-rw-r--r--data/templates/frr/bgpd.frr.j29
-rw-r--r--data/templates/frr/ldpd.frr.j210
-rw-r--r--data/templates/frr/policy.frr.j23
-rw-r--r--data/templates/frr/rpki.frr.j24
-rw-r--r--data/templates/https/nginx.default.j22
-rw-r--r--data/templates/ids/fastnetmon.j2121
-rw-r--r--data/templates/ids/fastnetmon_excluded_networks_list.j25
-rw-r--r--data/templates/ids/fastnetmon_networks_list.j25
-rw-r--r--data/templates/ipsec/charon_systemd.conf.j218
-rw-r--r--data/templates/ipsec/ios_profile.j26
-rw-r--r--data/templates/ipsec/swanctl/peer.j215
-rw-r--r--data/templates/lldp/vyos.conf.j211
-rw-r--r--data/templates/load-balancing/haproxy.cfg.j236
-rw-r--r--data/templates/load-balancing/nftables-wlb.j264
-rw-r--r--data/templates/load-balancing/wlb.conf.j2134
-rw-r--r--data/templates/login/motd_vyos_nonproduction.j21
-rw-r--r--data/templates/prometheus/node_exporter.service.j211
-rw-r--r--data/templates/router-advert/radvd.conf.j215
-rw-r--r--data/templates/rsyslog/override.conf.j211
-rw-r--r--data/templates/rsyslog/rsyslog.conf.j2163
-rw-r--r--data/templates/squid/squid.conf.j28
-rw-r--r--debian/control28
-rw-r--r--debian/vyos-1x.install1
-rw-r--r--debian/vyos-1x.postinst2
-rw-r--r--debian/vyos-1x.preinst1
-rw-r--r--interface-definitions/container.xml.in110
-rw-r--r--interface-definitions/firewall.xml.in13
-rw-r--r--interface-definitions/include/babel/redistribute-common.xml.i6
-rw-r--r--interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i62
-rw-r--r--interface-definitions/include/bgp/afi-route-map-export-import.xml.i34
-rw-r--r--interface-definitions/include/bgp/afi-route-map-export.xml.i18
-rw-r--r--interface-definitions/include/bgp/afi-route-map-import.xml.i18
-rw-r--r--interface-definitions/include/bgp/afi-route-map-vpn.xml.i3
-rw-r--r--interface-definitions/include/bgp/afi-route-map-vrf.xml.i17
-rw-r--r--interface-definitions/include/bgp/afi-route-map.xml.i3
-rw-r--r--interface-definitions/include/bgp/protocol-common-config.xml.i85
-rw-r--r--interface-definitions/include/constraint/interface-name.xml.i2
-rw-r--r--interface-definitions/include/constraint/protocols-static-table.xml.i9
-rw-r--r--interface-definitions/include/constraint/wireguard-keys.xml.i6
-rw-r--r--interface-definitions/include/dhcp/ddns-dns-server.xml.i19
-rw-r--r--interface-definitions/include/dhcp/ddns-settings.xml.i172
-rw-r--r--interface-definitions/include/dhcp/option-v4.xml.i12
-rw-r--r--interface-definitions/include/dhcp/option-v6.xml.i12
-rw-r--r--interface-definitions/include/dhcp/ping-check.xml.i8
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv4.xml.i2
-rw-r--r--interface-definitions/include/firewall/common-rule-ipv6.xml.i4
-rw-r--r--interface-definitions/include/firewall/global-options.xml.i8
-rw-r--r--interface-definitions/include/firewall/source-destination-remote-group.xml.i17
-rw-r--r--interface-definitions/include/haproxy/logging.xml.i132
-rw-r--r--interface-definitions/include/haproxy/timeout-check.xml.i14
-rw-r--r--interface-definitions/include/haproxy/timeout-client.xml.i14
-rw-r--r--interface-definitions/include/haproxy/timeout-connect.xml.i14
-rw-r--r--interface-definitions/include/haproxy/timeout-server.xml.i14
-rw-r--r--interface-definitions/include/haproxy/timeout.xml.i39
-rw-r--r--interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i15
-rw-r--r--interface-definitions/include/interface/ipv6-address.xml.i12
-rw-r--r--interface-definitions/include/interface/ipv6-options-with-nd.xml.i9
-rw-r--r--interface-definitions/include/interface/ipv6-options.xml.i11
-rw-r--r--interface-definitions/include/interface/vif-s.xml.i2
-rw-r--r--interface-definitions/include/interface/vif.xml.i1
-rw-r--r--interface-definitions/include/ip-address.xml.i14
-rw-r--r--interface-definitions/include/isis/protocol-common-config.xml.i8
-rw-r--r--interface-definitions/include/ospf/protocol-common-config.xml.i10
-rw-r--r--interface-definitions/include/policy/community-value-list.xml.i7
-rw-r--r--interface-definitions/include/version/bgp-version.xml.i2
-rw-r--r--interface-definitions/include/version/ids-version.xml.i2
-rw-r--r--interface-definitions/include/version/lldp-version.xml.i2
-rw-r--r--interface-definitions/include/version/policy-version.xml.i2
-rw-r--r--interface-definitions/include/version/reverseproxy-version.xml.i2
-rw-r--r--interface-definitions/include/version/system-version.xml.i2
-rw-r--r--interface-definitions/include/version/wanloadbalance-version.xml.i2
-rw-r--r--interface-definitions/interfaces_bonding.xml.in1
-rw-r--r--interface-definitions/interfaces_bridge.xml.in1
-rw-r--r--interface-definitions/interfaces_ethernet.xml.in1
-rw-r--r--interface-definitions/interfaces_geneve.xml.in5
-rw-r--r--interface-definitions/interfaces_l2tpv3.xml.in1
-rw-r--r--interface-definitions/interfaces_macsec.xml.in1
-rw-r--r--interface-definitions/interfaces_openvpn.xml.in1
-rw-r--r--interface-definitions/interfaces_pppoe.xml.in1
-rw-r--r--interface-definitions/interfaces_pseudo-ethernet.xml.in1
-rw-r--r--interface-definitions/interfaces_virtual-ethernet.xml.in4
-rw-r--r--interface-definitions/interfaces_vxlan.xml.in1
-rw-r--r--interface-definitions/interfaces_wireguard.xml.in19
-rw-r--r--interface-definitions/interfaces_wireless.xml.in1
-rw-r--r--interface-definitions/interfaces_wwan.xml.in1
-rw-r--r--interface-definitions/load-balancing_haproxy.xml.in35
-rw-r--r--interface-definitions/load-balancing_wan.xml.in2
-rw-r--r--interface-definitions/nat66.xml.in1
-rw-r--r--interface-definitions/policy.xml.in20
-rw-r--r--interface-definitions/policy_route.xml.in4
-rw-r--r--interface-definitions/protocols_mpls.xml.in24
-rw-r--r--interface-definitions/protocols_rip.xml.in8
-rw-r--r--interface-definitions/protocols_rpki.xml.in1
-rw-r--r--interface-definitions/protocols_static.xml.in10
-rw-r--r--interface-definitions/service_dhcp-server.xml.in136
-rw-r--r--interface-definitions/service_ids_ddos-protection.xml.in167
-rw-r--r--interface-definitions/service_ipoe-server.xml.in13
-rw-r--r--interface-definitions/service_lldp.xml.in29
-rw-r--r--interface-definitions/service_router-advert.xml.in13
-rw-r--r--interface-definitions/service_snmp.xml.in1
-rw-r--r--interface-definitions/system_option.xml.in152
-rw-r--r--interface-definitions/system_syslog.xml.in123
-rw-r--r--interface-definitions/vpn_ipsec.xml.in57
m---------libvyosconfig0
-rwxr-xr-xop-mode-definitions/firewall.xml.in7
-rw-r--r--op-mode-definitions/generate_tech-support_archive.xml.in17
-rw-r--r--op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i22
-rw-r--r--op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i1
-rw-r--r--op-mode-definitions/install-mok.xml.in2
-rw-r--r--op-mode-definitions/load-balancing_haproxy.xml.in (renamed from op-mode-definitions/load-balacing_haproxy.in)2
-rw-r--r--op-mode-definitions/load-balancing_wan.xml.in37
-rw-r--r--op-mode-definitions/monitor-log.xml.in19
-rw-r--r--op-mode-definitions/reset-session.xml.in (renamed from op-mode-definitions/clear-session.xml.in)2
-rw-r--r--op-mode-definitions/show-bridge.xml.in28
-rw-r--r--op-mode-definitions/show-interfaces.xml.in42
-rwxr-xr-xop-mode-definitions/show-log.xml.in54
-rw-r--r--op-mode-definitions/system-image.xml.in2
-rw-r--r--python/setup.py49
-rw-r--r--python/vyos/base.py21
-rw-r--r--python/vyos/component_version.py63
-rw-r--r--python/vyos/config.py12
-rw-r--r--python/vyos/config_mgmt.py10
-rw-r--r--python/vyos/configdep.py9
-rw-r--r--python/vyos/configdict.py37
-rw-r--r--python/vyos/configsession.py59
-rw-r--r--python/vyos/configsource.py10
-rw-r--r--python/vyos/configtree.py53
-rw-r--r--python/vyos/configverify.py6
-rw-r--r--python/vyos/defaults.py18
-rwxr-xr-xpython/vyos/firewall.py93
-rw-r--r--python/vyos/frrender.py11
-rw-r--r--python/vyos/ifconfig/bridge.py14
-rw-r--r--python/vyos/ifconfig/geneve.py2
-rw-r--r--python/vyos/ifconfig/interface.py195
-rw-r--r--python/vyos/ifconfig/wireguard.py78
-rw-r--r--python/vyos/kea.py133
-rw-r--r--python/vyos/proto/__init__.py0
-rwxr-xr-xpython/vyos/proto/generate_dataclass.py178
-rw-r--r--python/vyos/proto/vyconf_client.py89
-rw-r--r--python/vyos/system/grub_util.py5
-rwxr-xr-xpython/vyos/template.py162
-rw-r--r--python/vyos/utils/auth.py70
-rw-r--r--python/vyos/utils/cpu.py15
-rw-r--r--python/vyos/utils/misc.py2
-rw-r--r--python/vyos/utils/network.py92
-rw-r--r--python/vyos/utils/process.py48
-rw-r--r--python/vyos/vyconf_session.py123
-rw-r--r--python/vyos/wanloadbalance.py153
-rwxr-xr-xscripts/build-command-op-templates2
-rw-r--r--smoketest/config-tests/basic-api-service4
-rw-r--r--smoketest/config-tests/basic-haproxy46
-rw-r--r--smoketest/config-tests/basic-syslog25
-rw-r--r--smoketest/config-tests/basic-vyos35
-rw-r--r--smoketest/config-tests/basic-vyos-no-ntp4
-rw-r--r--smoketest/config-tests/bgp-azure-ipsec-gateway8
-rw-r--r--smoketest/config-tests/bgp-bfd-communities4
-rw-r--r--smoketest/config-tests/bgp-big-as-cloud4
-rw-r--r--smoketest/config-tests/bgp-dmvpn-hub4
-rw-r--r--smoketest/config-tests/bgp-dmvpn-spoke4
-rw-r--r--smoketest/config-tests/bgp-evpn-l2vpn-leaf4
-rw-r--r--smoketest/config-tests/bgp-evpn-l2vpn-spine4
-rw-r--r--smoketest/config-tests/bgp-evpn-l3vpn-pe-router4
-rw-r--r--smoketest/config-tests/bgp-medium-confederation4
-rw-r--r--smoketest/config-tests/bgp-rpki5
-rw-r--r--smoketest/config-tests/bgp-small-internet-exchange4
-rw-r--r--smoketest/config-tests/bgp-small-ipv4-unicast4
-rw-r--r--smoketest/config-tests/cluster-basic4
-rw-r--r--smoketest/config-tests/dialup-router-complex7
-rw-r--r--smoketest/config-tests/dialup-router-medium-vpn6
-rw-r--r--smoketest/config-tests/dialup-router-wireguard-ipv66
-rw-r--r--smoketest/config-tests/egp-igp-route-maps2
-rw-r--r--smoketest/config-tests/igmp-pim-small4
-rw-r--r--smoketest/config-tests/ipoe-server4
-rw-r--r--smoketest/config-tests/ipv6-disable4
-rw-r--r--smoketest/config-tests/isis-small4
-rw-r--r--smoketest/config-tests/nat-basic6
-rw-r--r--smoketest/config-tests/ospf-simple4
-rw-r--r--smoketest/config-tests/ospf-small4
-rw-r--r--smoketest/config-tests/pppoe-server4
-rw-r--r--smoketest/config-tests/qos-basic4
-rw-r--r--smoketest/config-tests/rip-router4
-rw-r--r--smoketest/config-tests/rpki-only4
-rw-r--r--smoketest/config-tests/static-route-basic4
-rw-r--r--smoketest/config-tests/tunnel-broker4
-rw-r--r--smoketest/config-tests/vpn-openconnect-sstp4
-rw-r--r--smoketest/config-tests/vrf-basic4
-rw-r--r--smoketest/config-tests/vrf-bgp-pppoe-underlay4
-rw-r--r--smoketest/config-tests/vrf-ospf4
-rw-r--r--smoketest/config-tests/wireless-basic4
-rw-r--r--smoketest/configs/basic-haproxy153
-rw-r--r--smoketest/configs/basic-syslog70
-rw-r--r--smoketest/configs/basic-vyos64
-rw-r--r--smoketest/configs/bgp-rpki7
-rw-r--r--smoketest/configs/dialup-router-complex3
-rw-r--r--smoketest/scripts/cli/base_interfaces_test.py124
-rw-r--r--smoketest/scripts/cli/base_vyostest_shim.py22
-rwxr-xr-xsmoketest/scripts/cli/test_container.py156
-rwxr-xr-xsmoketest/scripts/cli/test_firewall.py132
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bonding.py13
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_bridge.py15
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_loopback.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_virtual-ethernet.py5
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_vxlan.py26
-rwxr-xr-xsmoketest/scripts/cli/test_interfaces_wireless.py25
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_haproxy.py186
-rwxr-xr-xsmoketest/scripts/cli/test_load-balancing_wan.py156
-rwxr-xr-xsmoketest/scripts/cli/test_nat66.py29
-rwxr-xr-xsmoketest/scripts/cli/test_policy.py15
-rwxr-xr-xsmoketest/scripts/cli/test_policy_route.py34
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_babel.py4
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_bgp.py242
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_isis.py9
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_mpls.py71
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_nhrp.py3
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_ospf.py4
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rip.py4
-rwxr-xr-xsmoketest/scripts/cli/test_protocols_rpki.py36
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcp-server.py1104
-rwxr-xr-xsmoketest/scripts/cli/test_service_dhcpv6-server.py5
-rwxr-xr-xsmoketest/scripts/cli/test_service_https.py24
-rwxr-xr-xsmoketest/scripts/cli/test_service_ids_ddos-protection.py116
-rwxr-xr-xsmoketest/scripts/cli/test_service_ipoe-server.py26
-rwxr-xr-xsmoketest/scripts/cli/test_service_lldp.py49
-rwxr-xr-xsmoketest/scripts/cli/test_service_router-advert.py112
-rwxr-xr-xsmoketest/scripts/cli/test_service_webproxy.py18
-rwxr-xr-xsmoketest/scripts/cli/test_system_login.py18
-rwxr-xr-xsmoketest/scripts/cli/test_system_option.py62
-rwxr-xr-xsmoketest/scripts/cli/test_system_syslog.py293
-rwxr-xr-xsmoketest/scripts/cli/test_vpn_ipsec.py88
-rwxr-xr-xsmoketest/scripts/system/test_kernel_options.py9
-rwxr-xr-xsrc/conf_mode/container.py40
-rwxr-xr-xsrc/conf_mode/firewall.py52
-rwxr-xr-xsrc/conf_mode/interfaces_bonding.py28
-rwxr-xr-xsrc/conf_mode/interfaces_bridge.py7
-rwxr-xr-xsrc/conf_mode/interfaces_geneve.py2
-rwxr-xr-xsrc/conf_mode/interfaces_pseudo-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces_virtual-ethernet.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vti.py2
-rwxr-xr-xsrc/conf_mode/interfaces_vxlan.py2
-rwxr-xr-xsrc/conf_mode/interfaces_wireguard.py25
-rwxr-xr-xsrc/conf_mode/interfaces_wwan.py2
-rw-r--r--src/conf_mode/load-balancing_haproxy.py37
-rwxr-xr-xsrc/conf_mode/load-balancing_wan.py119
-rwxr-xr-xsrc/conf_mode/nat66.py4
-rwxr-xr-xsrc/conf_mode/pki.py106
-rwxr-xr-xsrc/conf_mode/policy_route.py47
-rwxr-xr-xsrc/conf_mode/protocols_bgp.py23
-rwxr-xr-xsrc/conf_mode/service_console-server.py8
-rwxr-xr-xsrc/conf_mode/service_dhcp-server.py47
-rwxr-xr-xsrc/conf_mode/service_dns_forwarding.py7
-rwxr-xr-xsrc/conf_mode/service_https.py14
-rwxr-xr-xsrc/conf_mode/service_ids_ddos-protection.py104
-rwxr-xr-xsrc/conf_mode/service_snmp.py8
-rwxr-xr-xsrc/conf_mode/system_host-name.py9
-rwxr-xr-xsrc/conf_mode/system_login.py16
-rwxr-xr-xsrc/conf_mode/system_login_banner.py8
-rwxr-xr-xsrc/conf_mode/system_option.py58
-rwxr-xr-xsrc/conf_mode/system_sflow.py2
-rwxr-xr-xsrc/conf_mode/system_syslog.py86
-rwxr-xr-xsrc/conf_mode/vpn_ipsec.py29
-rw-r--r--src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute20
-rwxr-xr-xsrc/etc/netplug/vyos-netplug-dhcp-client57
-rwxr-xr-xsrc/etc/ppp/ip-up.d/99-vyos-pppoe-wlb61
-rw-r--r--src/etc/rsyslog.conf67
-rw-r--r--src/etc/sysctl.d/30-vyos-router.conf10
-rw-r--r--src/etc/systemd/system/fastnetmon.service.d/override.conf12
-rw-r--r--src/etc/systemd/system/frr.service.d/override.conf6
-rw-r--r--src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf10
-rw-r--r--src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf7
-rw-r--r--src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf2
-rw-r--r--src/etc/systemd/system/rsyslog.service.d/override.conf10
-rwxr-xr-xsrc/helpers/geoip-update.py17
-rwxr-xr-xsrc/helpers/show_commit_data.py56
-rwxr-xr-xsrc/helpers/test_commit.py49
-rwxr-xr-xsrc/helpers/vyos-certbot-renew-pki.sh2
-rwxr-xr-xsrc/helpers/vyos-load-balancer.py312
-rwxr-xr-xsrc/init/vyos-router14
-rw-r--r--src/migration-scripts/bgp/5-to-639
-rw-r--r--src/migration-scripts/dhcp-server/7-to-86
-rw-r--r--src/migration-scripts/ids/1-to-230
-rw-r--r--src/migration-scripts/lldp/2-to-331
-rw-r--r--src/migration-scripts/policy/8-to-949
-rwxr-xr-xsrc/migration-scripts/reverse-proxy/2-to-366
-rw-r--r--src/migration-scripts/system/28-to-2971
-rw-r--r--src/migration-scripts/vrf/1-to-25
-rw-r--r--src/migration-scripts/vrf/2-to-33
-rw-r--r--src/migration-scripts/wanloadbalance/3-to-433
-rwxr-xr-xsrc/op_mode/dhcp.py18
-rwxr-xr-xsrc/op_mode/firewall.py159
-rwxr-xr-xsrc/op_mode/image_installer.py193
-rwxr-xr-xsrc/op_mode/interfaces.py138
-rwxr-xr-xsrc/op_mode/load-balancing_wan.py117
-rwxr-xr-xsrc/op_mode/qos.py2
-rwxr-xr-xsrc/op_mode/restart.py5
-rwxr-xr-xsrc/op_mode/stp.py185
-rw-r--r--src/op_mode/tech_support.py10
-rw-r--r--src/services/api/rest/models.py7
-rwxr-xr-xsrc/services/vyos-commitd457
-rwxr-xr-xsrc/services/vyos-configd44
-rwxr-xr-xsrc/services/vyos-conntrack-logger2
-rwxr-xr-xsrc/services/vyos-domain-resolver160
-rwxr-xr-xsrc/services/vyos-hostsd4
-rwxr-xr-xsrc/services/vyos-http-api-server46
-rwxr-xr-xsrc/system/sync-dhcp-lease-to-hosts.py112
-rw-r--r--src/systemd/netplug.service9
-rw-r--r--src/systemd/vyos-commitd.service27
-rw-r--r--src/systemd/vyos-wan-load-balance.service12
-rw-r--r--src/systemd/vyos.target2
-rw-r--r--src/tests/test_config_diff.py11
-rw-r--r--src/tests/test_config_parser.py4
-rw-r--r--src/tests/test_template.py9
-rw-r--r--src/tests/test_utils_network.py11
-rwxr-xr-xsrc/validators/base6422
-rwxr-xr-xsrc/validators/cpu43
-rw-r--r--src/validators/ethernet-interface13
-rw-r--r--tests/data/config.valid5
338 files changed, 9820 insertions, 2550 deletions
diff --git a/.github/workflows/mirror-pr-and-sync.yml b/.github/workflows/mirror-pr-and-sync.yml
index 48a67a43f..120e116d4 100644
--- a/.github/workflows/mirror-pr-and-sync.yml
+++ b/.github/workflows/mirror-pr-and-sync.yml
@@ -2,10 +2,6 @@ name: Create Mirror PR and Repo Sync
on:
workflow_dispatch:
inputs:
- pr_number:
- description: 'Source repo PR Number'
- required: true
- type: string
sync_branch:
description: 'branch to sync'
required: true
@@ -20,7 +16,6 @@ jobs:
if: github.repository_owner != 'vyos'
uses: VyOS-Networks/vyos-reusable-workflows/.github/workflows/mirror-pr-and-sync.yml@main
with:
- pr_number: ${{ inputs.pr_number }}
sync_branch: ${{ inputs.sync_branch }}
secrets:
PAT: ${{ secrets.PAT }}
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml
index ae34ea2f0..5ed764217 100644
--- a/.github/workflows/package-smoketest.yml
+++ b/.github/workflows/package-smoketest.yml
@@ -24,6 +24,7 @@ jobs:
build_iso:
runs-on: ubuntu-24.04
timeout-minutes: 45
+ if: github.repository == 'vyos/vyos-1x'
container:
image: vyos/vyos-build:current
options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged
@@ -41,6 +42,7 @@ jobs:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha }}
repository: ${{ github.event.pull_request.head.repo.full_name }}
+ submodules: true
- name: Build vyos-1x package
run: |
cd packages/vyos-1x; dpkg-buildpackage -uc -us -tc -b
@@ -63,6 +65,7 @@ jobs:
generic
- uses: actions/upload-artifact@v4
with:
+ retention-days: 2
name: vyos-${{ steps.version.outputs.build_version }}
path: |
build/live-image-amd64.hybrid.iso
diff --git a/.github/workflows/trigger-pr-mirror-repo-sync.yml b/.github/workflows/trigger-pr-mirror-repo-sync.yml
index f74895987..978be0582 100644
--- a/.github/workflows/trigger-pr-mirror-repo-sync.yml
+++ b/.github/workflows/trigger-pr-mirror-repo-sync.yml
@@ -6,6 +6,11 @@ on:
branches:
- current
+permissions:
+ pull-requests: write
+ contents: write
+ issues: write
+
jobs:
call-trigger-mirror-pr-repo-sync:
if: github.repository_owner == 'vyos' && github.event.pull_request.merged == true
diff --git a/.gitignore b/.gitignore
index d1bfc91d7..839d2afff 100644
--- a/.gitignore
+++ b/.gitignore
@@ -151,6 +151,11 @@ data/reftree.cache
# autogenerated vyos-configd JSON definition
data/configd-include.json
+# autogenerated vyos-commitd protobuf files
+python/vyos/proto/*pb2.py
+python/vyos/proto/*.desc
+python/vyos/proto/vyconf_proto.py
+
# We do not use pip
Pipfile
Pipfile.lock
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 000000000..05eaf619f
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,4 @@
+[submodule "libvyosconfig"]
+ path = libvyosconfig
+ url = ../../vyos/libvyosconfig
+ branch = current
diff --git a/CODEOWNERS b/CODEOWNERS
index 4891a0325..0bf2e6d79 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -1,2 +1,2 @@
# Users from reviewers github team
-* @dmbaturin @sarthurdev @jestabro @sever-sever @c-po @fett0 @nicolas-fort @zdc
+# * @vyos/reviewers
diff --git a/Makefile b/Makefile
index b5d114e59..14fefe208 100644
--- a/Makefile
+++ b/Makefile
@@ -7,7 +7,9 @@ LIBS := -lzmq
CFLAGS :=
BUILD_ARCH := $(shell dpkg-architecture -q DEB_BUILD_ARCH)
J2LINT := $(shell command -v j2lint 2> /dev/null)
-PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts)
+PYLINT_FILES := $(shell git ls-files *.py src/migration-scripts src/services)
+LIBVYOSCONFIG_BUILD_PATH := /tmp/libvyosconfig/_build/libvyosconfig.so
+LIBVYOSCONFIG_STATUS := $(shell git submodule status)
config_xml_src = $(wildcard interface-definitions/*.xml.in)
config_xml_obj = $(config_xml_src:.xml.in=.xml)
@@ -19,9 +21,21 @@ op_xml_obj = $(op_xml_src:.xml.in=.xml)
mkdir -p $(BUILD_DIR)/$(dir $@)
$(CURDIR)/scripts/transclude-template $< > $(BUILD_DIR)/$@
+.PHONY: libvyosconfig
+.ONESHELL:
+libvyosconfig:
+ if test ! -f $(LIBVYOSCONFIG_BUILD_PATH); then
+ if ! echo $(firstword $(LIBVYOSCONFIG_STATUS))|grep -Eq '^[a-z0-9]'; then
+ git submodule sync; git submodule update --init --remote
+ fi
+ rm -rf /tmp/libvyosconfig && mkdir /tmp/libvyosconfig
+ cp -r libvyosconfig /tmp && cd /tmp/libvyosconfig && \
+ eval $$(opam env --root=/opt/opam --set-root) && ./build.sh || exit 1
+ fi
+
.PHONY: interface_definitions
.ONESHELL:
-interface_definitions: $(config_xml_obj)
+interface_definitions: $(config_xml_obj) libvyosconfig
mkdir -p $(TMPL_DIR)
$(CURDIR)/scripts/override-default $(BUILD_DIR)/interface-definitions
@@ -75,7 +89,7 @@ vyshim:
$(MAKE) -C $(SHIM_DIR)
.PHONY: all
-all: clean interface_definitions op_mode_definitions test j2lint vyshim generate-configd-include-json
+all: clean libvyosconfig interface_definitions op_mode_definitions test j2lint vyshim generate-configd-include-json
.PHONY: clean
clean:
diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json
index cbd14f7c6..7506a0908 100644
--- a/data/config-mode-dependencies/vyos-1x.json
+++ b/data/config-mode-dependencies/vyos-1x.json
@@ -14,6 +14,9 @@
"vxlan": ["interfaces_vxlan"],
"wlan": ["interfaces_wireless"]
},
+ "interfaces_wireguard": {
+ "vxlan": ["interfaces_vxlan"]
+ },
"load_balancing_wan": {
"conntrack": ["system_conntrack"]
},
diff --git a/data/config.boot.default b/data/config.boot.default
index 93369d9b7..db5d11ea1 100644
--- a/data/config.boot.default
+++ b/data/config.boot.default
@@ -41,7 +41,7 @@ system {
}
}
syslog {
- global {
+ local {
facility all {
level "info"
}
diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json
index 170f0d259..5d3f4a249 100644
--- a/data/op-mode-standardized.json
+++ b/data/op-mode-standardized.json
@@ -13,6 +13,7 @@
"evpn.py",
"interfaces.py",
"ipsec.py",
+"load-balancing_wan.py",
"lldp.py",
"log.py",
"memory.py",
@@ -27,6 +28,7 @@
"load-balancing_haproxy.py",
"route.py",
"storage.py",
+"stp.py",
"system.py",
"uptime.py",
"version.py",
diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.j2 b/data/templates/accel-ppp/chap-secrets.ipoe.j2
index dd85160c0..59b9dfc8d 100644
--- a/data/templates/accel-ppp/chap-secrets.ipoe.j2
+++ b/data/templates/accel-ppp/chap-secrets.ipoe.j2
@@ -6,7 +6,7 @@
{% if mac_config.vlan is vyos_defined %}
{% set iface = iface ~ '.' ~ mac_config.vlan %}
{% endif %}
-{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.static_ip if mac_config.static_ip is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }}
+{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.ip_address if mac_config.ip_address is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }}
{% endfor %}
{% endif %}
{% endfor %}
diff --git a/data/templates/conserver/dropbear@.service.j2 b/data/templates/conserver/dropbear@.service.j2
index e355dab43..c6c31f98f 100644
--- a/data/templates/conserver/dropbear@.service.j2
+++ b/data/templates/conserver/dropbear@.service.j2
@@ -1,4 +1,4 @@
[Service]
ExecStart=
-ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
+ExecStart=/usr/sbin/dropbear -w -j -k -r /etc/dropbear/dropbear_rsa_host_key -r /etc/dropbear/dropbear_ecdsa_host_key -b /etc/issue.net -c "/usr/bin/console {{ device }}" -P /run/conserver/dropbear.%I.pid -p %I
PIDFile=/run/conserver/dropbear.%I.pid
diff --git a/data/templates/container/containers.conf.j2 b/data/templates/container/containers.conf.j2
index c8b54dfbb..65436801e 100644
--- a/data/templates/container/containers.conf.j2
+++ b/data/templates/container/containers.conf.j2
@@ -172,7 +172,11 @@ default_sysctls = [
# Logging driver for the container. Available options: k8s-file and journald.
#
+{% if log_driver is vyos_defined %}
+log_driver = "{{ log_driver }}"
+{% else %}
#log_driver = "k8s-file"
+{% endif %}
# Maximum size allowed for the container log file. Negative numbers indicate
# that no size limit is imposed. If positive, it must be >= 8192 to match or
diff --git a/data/templates/container/registries.conf.j2 b/data/templates/container/registries.conf.j2
index eb7ff8775..b5c7eed9b 100644
--- a/data/templates/container/registries.conf.j2
+++ b/data/templates/container/registries.conf.j2
@@ -28,4 +28,14 @@
{% set _ = registry_list.append(r) %}
{% endfor %}
unqualified-search-registries = {{ registry_list }}
+{% for r, r_options in registry.items() if r_options.disable is not vyos_defined %}
+[[registry]]
+{% if r_options.mirror is vyos_defined %}
+location = "{{ r_options.mirror.host_name if r_options.mirror.host_name is vyos_defined else r_options.mirror.address }}{{ ":" + r_options.mirror.port if r_options.mirror.port is vyos_defined }}{{ r_options.mirror.path if r_options.mirror.path is vyos_defined }}"
+{% else %}
+location = "{{ r }}"
+{% endif %}
+insecure = {{ 'true' if r_options.insecure is vyos_defined else 'false' }}
+prefix = "{{ r }}"
+{% endfor %}
{% endif %}
diff --git a/data/templates/dhcp-client/ipv6.override.conf.j2 b/data/templates/dhcp-client/ipv6.override.conf.j2
index b0c0e0544..d270a55fc 100644
--- a/data/templates/dhcp-client/ipv6.override.conf.j2
+++ b/data/templates/dhcp-client/ipv6.override.conf.j2
@@ -4,6 +4,9 @@
[Unit]
ConditionPathExists={{ dhcp6_client_dir }}/dhcp6c.%i.conf
+{% if ifname.startswith('pppoe') %}
+After=ppp@{{ ifname }}.service
+{% endif %}
[Service]
ExecStart=
diff --git a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2
deleted file mode 100644
index b37cf4798..000000000
--- a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2
+++ /dev/null
@@ -1,14 +0,0 @@
-{
- "Control-agent": {
-{% if high_availability is vyos_defined %}
- "http-host": "{{ high_availability.source_address }}",
- "http-port": 647,
- "control-sockets": {
- "dhcp4": {
- "socket-type": "unix",
- "socket-name": "/run/kea/dhcp4-ctrl-socket"
- }
- }
-{% endif %}
- }
-}
diff --git a/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2 b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2
new file mode 100644
index 000000000..7b0394a88
--- /dev/null
+++ b/data/templates/dhcp-server/kea-dhcp-ddns.conf.j2
@@ -0,0 +1,30 @@
+{
+ "DhcpDdns": {
+ "ip-address": "127.0.0.1",
+ "port": 53001,
+ "control-socket": {
+ "socket-type": "unix",
+ "socket-name": "/run/kea/kea-ddns-ctrl-socket"
+ },
+ "tsig-keys": {{ dynamic_dns_update | kea_dynamic_dns_update_tsig_key_json }},
+ "forward-ddns" : {
+ "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('forward_domain') }}
+ },
+ "reverse-ddns" : {
+ "ddns-domains": {{ dynamic_dns_update | kea_dynamic_dns_update_domains('reverse_domain') }}
+ },
+ "loggers": [
+ {
+ "name": "kea-dhcp-ddns",
+ "output_options": [
+ {
+ "output": "stdout",
+ "pattern": "%-5p %m\n"
+ }
+ ],
+ "severity": "INFO",
+ "debuglevel": 0
+ }
+ ]
+ }
+}
diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2
index 2e10d58e0..d08ca0eaa 100644
--- a/data/templates/dhcp-server/kea-dhcp4.conf.j2
+++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2
@@ -25,20 +25,6 @@
},
"option-def": [
{
- "name": "rfc3442-static-route",
- "code": 121,
- "type": "record",
- "array": true,
- "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8"
- },
- {
- "name": "windows-static-route",
- "code": 249,
- "type": "record",
- "array": true,
- "record-types": "uint8,uint8,uint8,uint8,uint8,uint8,uint8"
- },
- {
"name": "wpad-url",
"code": 252,
"type": "string"
@@ -50,6 +36,19 @@
"space": "ubnt"
}
],
+{% if dynamic_dns_update is vyos_defined %}
+ "dhcp-ddns": {
+ "enable-updates": true,
+ "server-ip": "127.0.0.1",
+ "server-port": 53001,
+ "sender-ip": "",
+ "sender-port": 0,
+ "max-queue-size": 1024,
+ "ncr-protocol": "UDP",
+ "ncr-format": "JSON"
+ },
+ {{ dynamic_dns_update | kea_dynamic_dns_update_main_json }}
+{% endif %}
"hooks-libraries": [
{% if high_availability is vyos_defined %}
{
@@ -69,6 +68,16 @@
},
{% endif %}
{
+ "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ping_check.so",
+ "parameters": {
+ "enable-ping-check" : false,
+ "min-ping-requests" : 1,
+ "reply-timeout" : 100,
+ "ping-cltt-secs" : 60,
+ "ping-channel-threads" : 0
+ }
+ },
+ {
"library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_lease_cmds.so",
"parameters": {}
}
diff --git a/data/templates/firewall/nftables-defines.j2 b/data/templates/firewall/nftables-defines.j2
index fa6cd74c0..a1d1fa4f6 100644
--- a/data/templates/firewall/nftables-defines.j2
+++ b/data/templates/firewall/nftables-defines.j2
@@ -35,6 +35,24 @@
}
{% endfor %}
{% endif %}
+{% if group.remote_group is vyos_defined and is_l3 and not is_ipv6 %}
+{% for name, name_config in group.remote_group.items() %}
+ set R_{{ name }} {
+ type {{ ip_type }}
+ flags interval
+ auto-merge
+ }
+{% endfor %}
+{% endif %}
+{% if group.remote_group is vyos_defined and is_l3 and is_ipv6 %}
+{% for name, name_config in group.remote_group.items() %}
+ set R6_{{ name }} {
+ type {{ ip_type }}
+ flags interval
+ auto-merge
+ }
+{% endfor %}
+{% endif %}
{% if group.mac_group is vyos_defined %}
{% for group_name, group_conf in group.mac_group.items() %}
{% set includes = group_conf.include if group_conf.include is vyos_defined else [] %}
diff --git a/data/templates/firewall/nftables-geoip-update.j2 b/data/templates/firewall/nftables-geoip-update.j2
index 832ccc3e9..d8f80d1f5 100644
--- a/data/templates/firewall/nftables-geoip-update.j2
+++ b/data/templates/firewall/nftables-geoip-update.j2
@@ -31,3 +31,36 @@ table ip6 vyos_filter {
{% endfor %}
}
{% endif %}
+
+
+{% if ipv4_sets_policy is vyos_defined %}
+{% for setname, ip_list in ipv4_sets_policy.items() %}
+flush set ip vyos_mangle {{ setname }}
+{% endfor %}
+
+table ip vyos_mangle {
+{% for setname, ip_list in ipv4_sets_policy.items() %}
+ set {{ setname }} {
+ type ipv4_addr
+ flags interval
+ elements = { {{ ','.join(ip_list) }} }
+ }
+{% endfor %}
+}
+{% endif %}
+
+{% if ipv6_sets_policy is vyos_defined %}
+{% for setname, ip_list in ipv6_sets_policy.items() %}
+flush set ip6 vyos_mangle {{ setname }}
+{% endfor %}
+
+table ip6 vyos_mangle {
+{% for setname, ip_list in ipv6_sets_policy.items() %}
+ set {{ setname }} {
+ type ipv6_addr
+ flags interval
+ elements = { {{ ','.join(ip_list) }} }
+ }
+{% endfor %}
+}
+{% endif %}
diff --git a/data/templates/firewall/nftables-policy.j2 b/data/templates/firewall/nftables-policy.j2
index 9e28899b0..00d0e8a62 100644
--- a/data/templates/firewall/nftables-policy.j2
+++ b/data/templates/firewall/nftables-policy.j2
@@ -33,6 +33,15 @@ table ip vyos_mangle {
{% endif %}
}
{% endfor %}
+
+{% if geoip_updated.name is vyos_defined %}
+{% for setname in geoip_updated.name %}
+ set {{ setname }} {
+ type ipv4_addr
+ flags interval
+ }
+{% endfor %}
+{% endif %}
{% endif %}
{{ group_tmpl.groups(firewall_group, False, True) }}
@@ -65,6 +74,14 @@ table ip6 vyos_mangle {
{% endif %}
}
{% endfor %}
+{% if geoip_updated.ipv6_name is vyos_defined %}
+{% for setname in geoip_updated.ipv6_name %}
+ set {{ setname }} {
+ type ipv6_addr
+ flags interval
+ }
+{% endfor %}
+{% endif %}
{% endif %}
{{ group_tmpl.groups(firewall_group, True, True) }}
diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2
index a35143870..a78119a80 100755
--- a/data/templates/firewall/nftables.j2
+++ b/data/templates/firewall/nftables.j2
@@ -47,7 +47,7 @@ table ip vyos_filter {
chain VYOS_FORWARD_{{ prior }} {
type filter hook forward priority {{ prior }}; policy accept;
{% if global_options.state_policy is vyos_defined %}
- jump VYOS_STATE_POLICY
+ jump VYOS_STATE_POLICY_FORWARD
{% endif %}
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
@@ -180,6 +180,22 @@ table ip vyos_filter {
{% endif %}
return
}
+
+ chain VYOS_STATE_POLICY_FORWARD {
+{% if global_options.state_policy.offload is vyos_defined %}
+ counter flow add @VYOS_FLOWTABLE_{{ global_options.state_policy.offload.offload_target }}
+{% endif %}
+{% if global_options.state_policy.established is vyos_defined %}
+ {{ global_options.state_policy.established | nft_state_policy('established') }}
+{% endif %}
+{% if global_options.state_policy.invalid is vyos_defined %}
+ {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+{% endif %}
+{% if global_options.state_policy.related is vyos_defined %}
+ {{ global_options.state_policy.related | nft_state_policy('related') }}
+{% endif %}
+ return
+ }
{% endif %}
}
@@ -200,7 +216,7 @@ table ip6 vyos_filter {
chain VYOS_IPV6_FORWARD_{{ prior }} {
type filter hook forward priority {{ prior }}; policy accept;
{% if global_options.state_policy is vyos_defined %}
- jump VYOS_STATE_POLICY6
+ jump VYOS_STATE_POLICY6_FORWARD
{% endif %}
{% if conf.rule is vyos_defined %}
{% for rule_id, rule_conf in conf.rule.items() if rule_conf.disable is not vyos_defined %}
@@ -331,6 +347,22 @@ table ip6 vyos_filter {
{% endif %}
return
}
+
+ chain VYOS_STATE_POLICY6_FORWARD {
+{% if global_options.state_policy.offload is vyos_defined %}
+ counter flow add @VYOS_FLOWTABLE_{{ global_options.state_policy.offload.offload_target }}
+{% endif %}
+{% if global_options.state_policy.established is vyos_defined %}
+ {{ global_options.state_policy.established | nft_state_policy('established') }}
+{% endif %}
+{% if global_options.state_policy.invalid is vyos_defined %}
+ {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+{% endif %}
+{% if global_options.state_policy.related is vyos_defined %}
+ {{ global_options.state_policy.related | nft_state_policy('related') }}
+{% endif %}
+ return
+ }
{% endif %}
}
@@ -435,13 +467,13 @@ table bridge vyos_filter {
{% if global_options.state_policy is vyos_defined %}
chain VYOS_STATE_POLICY {
{% if global_options.state_policy.established is vyos_defined %}
- {{ global_options.state_policy.established | nft_state_policy('established') }}
+ {{ global_options.state_policy.established | nft_state_policy('established', bridge=True) }}
{% endif %}
{% if global_options.state_policy.invalid is vyos_defined %}
- {{ global_options.state_policy.invalid | nft_state_policy('invalid') }}
+ {{ global_options.state_policy.invalid | nft_state_policy('invalid', bridge=True) }}
{% endif %}
{% if global_options.state_policy.related is vyos_defined %}
- {{ global_options.state_policy.related | nft_state_policy('related') }}
+ {{ global_options.state_policy.related | nft_state_policy('related', bridge=True) }}
{% endif %}
return
}
diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2
index 51a3f2564..e153dd4e8 100644
--- a/data/templates/frr/bgpd.frr.j2
+++ b/data/templates/frr/bgpd.frr.j2
@@ -98,6 +98,8 @@
{% endif %}
{% if config.enforce_first_as is vyos_defined %}
neighbor {{ neighbor }} enforce-first-as
+{% else %}
+ no neighbor {{ neighbor }} enforce-first-as
{% endif %}
{% if config.strict_capability_match is vyos_defined %}
neighbor {{ neighbor }} strict-capability-match
@@ -310,7 +312,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}
{% if afi_config.redistribute is vyos_defined %}
{% for protocol, protocol_config in afi_config.redistribute.items() %}
{% if protocol == 'table' %}
- redistribute table {{ protocol_config.table }}
+{% for table, table_config in protocol_config.items() %}
+ redistribute table-direct {{ table }} {{ 'metric ' ~ table_config.metric if table_config.metric is vyos_defined }} {{ 'route-map ' ~ table_config.route_map if table_config.route_map is vyos_defined }}
+{% endfor %}
{% else %}
{% set redistribution_protocol = protocol %}
{% if protocol == 'ospfv3' %}
@@ -355,6 +359,9 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }}
import vrf {{ vrf }}
{% endfor %}
{% endif %}
+{% if afi_config.route_map.vrf.import is vyos_defined %}
+ import vrf route-map {{ afi_config.route_map.vrf.import }}
+{% endif %}
{% if afi_config.label.vpn.export is vyos_defined %}
label vpn export {{ afi_config.label.vpn.export }}
{% endif %}
diff --git a/data/templates/frr/ldpd.frr.j2 b/data/templates/frr/ldpd.frr.j2
index 9a893cc55..b8fb0cfc7 100644
--- a/data/templates/frr/ldpd.frr.j2
+++ b/data/templates/frr/ldpd.frr.j2
@@ -82,8 +82,11 @@ mpls ldp
{% endfor %}
{% endif %}
{% if ldp.interface is vyos_defined %}
-{% for interface in ldp.interface %}
+{% for interface, iface_config in ldp.interface.items() %}
interface {{ interface }}
+{% if iface_config.disable_establish_hello is vyos_defined %}
+ disable-establish-hello
+{% endif %}
exit
{% endfor %}
{% endif %}
@@ -135,8 +138,11 @@ mpls ldp
{% endfor %}
{% endif %}
{% if ldp.interface is vyos_defined %}
-{% for interface in ldp.interface %}
+{% for interface, iface_config in ldp.interface.items() %}
interface {{ interface }}
+{% if iface_config.disable_establish_hello is vyos_defined %}
+ disable-establish-hello
+{% endif %}
{% endfor %}
{% endif %}
exit-address-family
diff --git a/data/templates/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2
index ed5876ae9..c28633f6f 100644
--- a/data/templates/frr/policy.frr.j2
+++ b/data/templates/frr/policy.frr.j2
@@ -252,6 +252,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }}
{% if rule_config.match.rpki is vyos_defined %}
match rpki {{ rule_config.match.rpki }}
{% endif %}
+{% if rule_config.match.source_vrf is vyos_defined %}
+ match source-vrf {{ rule_config.match.source_vrf }}
+{% endif %}
{% if rule_config.match.tag is vyos_defined %}
match tag {{ rule_config.match.tag }}
{% endif %}
diff --git a/data/templates/frr/rpki.frr.j2 b/data/templates/frr/rpki.frr.j2
index 59d5bf0ac..edf0ccaa2 100644
--- a/data/templates/frr/rpki.frr.j2
+++ b/data/templates/frr/rpki.frr.j2
@@ -5,9 +5,9 @@ rpki
{% for peer, peer_config in cache.items() %}
{# port is mandatory and preference uses a default value #}
{% if peer_config.ssh.username is vyos_defined %}
- rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} preference {{ peer_config.preference }}
+ rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }}
{% else %}
- rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }} preference {{ peer_config.preference }}
+ rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }}
{% endif %}
{% endfor %}
{% endif %}
diff --git a/data/templates/https/nginx.default.j2 b/data/templates/https/nginx.default.j2
index 51da46946..692ccbff7 100644
--- a/data/templates/https/nginx.default.j2
+++ b/data/templates/https/nginx.default.j2
@@ -48,7 +48,7 @@ server {
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';
# proxy settings for HTTP API, if enabled; 503, if not
- location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|traceroute|docs|openapi.json|redoc|graphql) {
+ location ~ ^/(retrieve|configure|config-file|image|import-pki|container-image|generate|show|reboot|reset|poweroff|traceroute|info|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;
diff --git a/data/templates/ids/fastnetmon.j2 b/data/templates/ids/fastnetmon.j2
deleted file mode 100644
index f6f03d0db..000000000
--- a/data/templates/ids/fastnetmon.j2
+++ /dev/null
@@ -1,121 +0,0 @@
-# enable this option if you want to send logs to local syslog facility
-logging:logging_level = debug
-logging:local_syslog_logging = on
-
-# list of all your networks in CIDR format
-networks_list_path = /run/fastnetmon/networks_list
-
-# list networks in CIDR format which will be not monitored for attacks
-white_list_path = /run/fastnetmon/excluded_networks_list
-
-# Enable/Disable any actions in case of attack
-enable_ban = on
-enable_ban_ipv6 = on
-
-## How many packets will be collected from attack traffic
-ban_details_records_count = 500
-
-## How long (in seconds) we should keep an IP in blocked state
-## If you set 0 here it completely disables unban capability
-{% if ban_time is vyos_defined %}
-ban_time = {{ ban_time }}
-{% endif %}
-
-# Check if the attack is still active, before triggering an unban callback with this option
-# If the attack is still active, check each run of the unban watchdog
-unban_only_if_attack_finished = on
-
-# enable per subnet speed meters
-# For each subnet, list track speed in bps and pps for both directions
-enable_subnet_counters = off
-
-{% if mode is vyos_defined('mirror') %}
-mirror_afpacket = on
-{% elif mode is vyos_defined('sflow') %}
-sflow = on
-{% if sflow.port is vyos_defined %}
-sflow_port = {{ sflow.port }}
-{% endif %}
-{% if sflow.listen_address is vyos_defined %}
-sflow_host = {{ sflow.listen_address }}
-{% endif %}
-{% endif %}
-
-
-process_incoming_traffic = {{ 'on' if direction is vyos_defined and 'in' in direction else 'off' }}
-process_outgoing_traffic = {{ 'on' if direction is vyos_defined and 'out' in direction else 'off' }}
-
-{% if threshold is vyos_defined %}
-{% if threshold.general is vyos_defined %}
-# General threshold
-{% for thr, thr_value in threshold.general.items() %}
-{% if thr is vyos_defined('fps') %}
-ban_for_flows = on
-threshold_flows = {{ thr_value }}
-{% elif thr is vyos_defined('mbps') %}
-ban_for_bandwidth = on
-threshold_mbps = {{ thr_value }}
-{% elif thr is vyos_defined('pps') %}
-ban_for_pps = on
-threshold_pps = {{ thr_value }}
-{% endif %}
-{% endfor %}
-{% endif %}
-
-{% if threshold.tcp is vyos_defined %}
-# TCP threshold
-{% for thr, thr_value in threshold.tcp.items() %}
-{% if thr is vyos_defined('fps') %}
-ban_for_tcp_flows = on
-threshold_tcp_flows = {{ thr_value }}
-{% elif thr is vyos_defined('mbps') %}
-ban_for_tcp_bandwidth = on
-threshold_tcp_mbps = {{ thr_value }}
-{% elif thr is vyos_defined('pps') %}
-ban_for_tcp_pps = on
-threshold_tcp_pps = {{ thr_value }}
-{% endif %}
-{% endfor %}
-{% endif %}
-
-{% if threshold.udp is vyos_defined %}
-# UDP threshold
-{% for thr, thr_value in threshold.udp.items() %}
-{% if thr is vyos_defined('fps') %}
-ban_for_udp_flows = on
-threshold_udp_flows = {{ thr_value }}
-{% elif thr is vyos_defined('mbps') %}
-ban_for_udp_bandwidth = on
-threshold_udp_mbps = {{ thr_value }}
-{% elif thr is vyos_defined('pps') %}
-ban_for_udp_pps = on
-threshold_udp_pps = {{ thr_value }}
-{% endif %}
-{% endfor %}
-{% endif %}
-
-{% if threshold.icmp is vyos_defined %}
-# ICMP threshold
-{% for thr, thr_value in threshold.icmp.items() %}
-{% if thr is vyos_defined('fps') %}
-ban_for_icmp_flows = on
-threshold_icmp_flows = {{ thr_value }}
-{% elif thr is vyos_defined('mbps') %}
-ban_for_icmp_bandwidth = on
-threshold_icmp_mbps = {{ thr_value }}
-{% elif thr is vyos_defined('pps') %}
-ban_for_icmp_pps = on
-threshold_icmp_pps = {{ thr_value }}
-{% endif %}
-{% endfor %}
-{% endif %}
-
-{% endif %}
-
-{% if listen_interface is vyos_defined %}
-interfaces = {{ listen_interface | join(',') }}
-{% endif %}
-
-{% if alert_script is vyos_defined %}
-notify_script_path = {{ alert_script }}
-{% endif %}
diff --git a/data/templates/ids/fastnetmon_excluded_networks_list.j2 b/data/templates/ids/fastnetmon_excluded_networks_list.j2
deleted file mode 100644
index c88a1c527..000000000
--- a/data/templates/ids/fastnetmon_excluded_networks_list.j2
+++ /dev/null
@@ -1,5 +0,0 @@
-{% if excluded_network is vyos_defined %}
-{% for net in excluded_network %}
-{{ net }}
-{% endfor %}
-{% endif %}
diff --git a/data/templates/ids/fastnetmon_networks_list.j2 b/data/templates/ids/fastnetmon_networks_list.j2
deleted file mode 100644
index 0a0576d2a..000000000
--- a/data/templates/ids/fastnetmon_networks_list.j2
+++ /dev/null
@@ -1,5 +0,0 @@
-{% if network is vyos_defined %}
-{% for net in network %}
-{{ net }}
-{% endfor %}
-{% endif %}
diff --git a/data/templates/ipsec/charon_systemd.conf.j2 b/data/templates/ipsec/charon_systemd.conf.j2
new file mode 100644
index 000000000..368aa1ae3
--- /dev/null
+++ b/data/templates/ipsec/charon_systemd.conf.j2
@@ -0,0 +1,18 @@
+# Generated by ${vyos_conf_scripts_dir}/vpn_ipsec.py
+
+charon-systemd {
+
+ # Section to configure native systemd journal logger, very similar to the
+ # syslog logger as described in LOGGER CONFIGURATION in strongswan.conf(5).
+ journal {
+
+ # Loglevel for a specific subsystem.
+ # <subsystem> = <default>
+
+{% if log.level is vyos_defined %}
+ # Default loglevel.
+ default = {{ log.level }}
+{% endif %}
+ }
+
+}
diff --git a/data/templates/ipsec/ios_profile.j2 b/data/templates/ipsec/ios_profile.j2
index 966fad433..6993f82bf 100644
--- a/data/templates/ipsec/ios_profile.j2
+++ b/data/templates/ipsec/ios_profile.j2
@@ -55,11 +55,9 @@
<!-- The server is authenticated using a certificate -->
<key>AuthenticationMethod</key>
<string>Certificate</string>
-{% if authentication.client_mode.startswith("eap") %}
<!-- The client uses EAP to authenticate -->
<key>ExtendedAuthEnabled</key>
<integer>1</integer>
-{% endif %}
<!-- The next two dictionaries are optional (as are the keys in them), but it is recommended to specify them as the default is to use 3DES.
IMPORTANT: Because only one proposal is sent (even if nothing is configured here) it must match the server configuration -->
<key>IKESecurityAssociationParameters</key>
@@ -80,9 +78,9 @@
<string>{{ esp_encryption.encryption }}</string>
<key>IntegrityAlgorithm</key>
<string>{{ esp_encryption.hash }}</string>
-{% if esp_encryption.pfs is vyos_defined %}
+{% if ike_encryption.dh_group is vyos_defined %}
<key>DiffieHellmanGroup</key>
- <integer>{{ esp_encryption.pfs }}</integer>
+ <integer>{{ ike_encryption.dh_group }}</integer>
{% endif %}
</dict>
<!-- Controls whether the client offers Perfect Forward Secrecy (PFS). This should be set to match the server. -->
diff --git a/data/templates/ipsec/swanctl/peer.j2 b/data/templates/ipsec/swanctl/peer.j2
index 3a9af2c94..cf0865c88 100644
--- a/data/templates/ipsec/swanctl/peer.j2
+++ b/data/templates/ipsec/swanctl/peer.j2
@@ -68,8 +68,19 @@
rekey_packets = 0
rekey_time = 0s
{% endif %}
- local_ts = 0.0.0.0/0,::/0
- remote_ts = 0.0.0.0/0,::/0
+{# set default traffic-selectors #}
+{% set local_ts = '0.0.0.0/0,::/0' %}
+{% set remote_ts = '0.0.0.0/0,::/0' %}
+{% if peer_conf.vti.traffic_selector is vyos_defined %}
+{% if peer_conf.vti.traffic_selector.local is vyos_defined and peer_conf.vti.traffic_selector.local.prefix is vyos_defined %}
+{% set local_ts = peer_conf.vti.traffic_selector.local.prefix | join(',') %}
+{% endif %}
+{% if peer_conf.vti.traffic_selector.remote is vyos_defined and peer_conf.vti.traffic_selector.remote.prefix is vyos_defined %}
+{% set remote_ts = peer_conf.vti.traffic_selector.remote.prefix | join(',') %}
+{% endif %}
+{% endif %}
+ local_ts = {{ local_ts }}
+ remote_ts = {{ remote_ts }}
updown = "/etc/ipsec.d/vti-up-down {{ peer_conf.vti.bind }}"
{# 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/lldp/vyos.conf.j2 b/data/templates/lldp/vyos.conf.j2
index 4b4228cea..432a7a8e6 100644
--- a/data/templates/lldp/vyos.conf.j2
+++ b/data/templates/lldp/vyos.conf.j2
@@ -4,7 +4,7 @@ configure system platform VyOS
configure system description "VyOS {{ version }}"
{% if interface is vyos_defined %}
{% set tmp = [] %}
-{% for iface, iface_options in interface.items() if iface_options.disable is not vyos_defined %}
+{% for iface, iface_options in interface.items() %}
{% if iface == 'all' %}
{% set iface = '*' %}
{% endif %}
@@ -17,6 +17,15 @@ configure ports {{ iface }} med location elin "{{ iface_options.location.elin }}
configure ports {{ iface }} med location coordinate latitude "{{ iface_options.location.coordinate_based.latitude }}" longitude "{{ iface_options.location.coordinate_based.longitude }}" altitude "{{ iface_options.location.coordinate_based.altitude }}m" datum "{{ iface_options.location.coordinate_based.datum }}"
{% endif %}
{% endif %}
+{% set mode = iface_options.mode %}
+{% if mode == 'tx' %}
+{% set mode = 'tx-only' %}
+{% elif mode == 'rx' %}
+{% set mode = 'rx-only' %}
+{% elif mode == 'rx-tx' %}
+{% set mode = 'rx-and-tx' %}
+{% endif %}
+configure ports {{ iface }} lldp status {{ mode }}
{% endfor %}
configure system interface pattern "{{ tmp | join(",") }}"
{% endif %}
diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2
index c98b739e2..62934c612 100644
--- a/data/templates/load-balancing/haproxy.cfg.j2
+++ b/data/templates/load-balancing/haproxy.cfg.j2
@@ -38,9 +38,10 @@ defaults
log global
mode http
option dontlognull
- timeout connect 10s
- timeout client 50s
- timeout server 50s
+ timeout check {{ timeout.check }}s
+ timeout connect {{ timeout.connect }}s
+ timeout client {{ timeout.client }}s
+ timeout server {{ timeout.server }}s
errorfile 400 /etc/haproxy/errors/400.http
errorfile 403 /etc/haproxy/errors/403.http
errorfile 408 /etc/haproxy/errors/408.http
@@ -49,9 +50,29 @@ defaults
errorfile 503 /etc/haproxy/errors/503.http
errorfile 504 /etc/haproxy/errors/504.http
+# Default ACME backend
+backend buildin_acme_certbot
+ server localhost 127.0.0.1:{{ get_default_port('certbot_haproxy') }}
+
# Frontend
{% if service is vyos_defined %}
{% for front, front_config in service.items() %}
+{% if front_config.redirect_http_to_https is vyos_defined %}
+{% set certbot_backend_name = 'certbot_' ~ front ~ '_backend' %}
+frontend {{ front }}-http
+ mode http
+{% if front_config.listen_address is vyos_defined %}
+{% for address in front_config.listen_address %}
+ bind {{ address | bracketize_ipv6 }}:80
+{% endfor %}
+{% else %}
+ bind [::]:80 v4v6
+{% endif %}
+ acl acme_acl path_beg /.well-known/acme-challenge/
+ use_backend buildin_acme_certbot if acme_acl
+ redirect scheme https code 301 if !acme_acl
+{% endif %}
+
frontend {{ front }}
{% set ssl_front = [] %}
{% if front_config.ssl.certificate is vyos_defined and front_config.ssl.certificate is iterable %}
@@ -67,9 +88,6 @@ frontend {{ front }}
{% else %}
bind [::]:{{ front_config.port }} v4v6 {{ ssl_directive }} {{ ssl_front | join(' ') }}
{% endif %}
-{% if front_config.redirect_http_to_https is vyos_defined %}
- http-request redirect scheme https unless { ssl_fc }
-{% endif %}
{% if front_config.logging is vyos_defined %}
{% for facility, facility_config in front_config.logging.facility.items() %}
log /dev/log {{ facility }} {{ facility_config.level }}
@@ -134,6 +152,11 @@ frontend {{ front }}
default_backend {{ backend }}
{% endfor %}
{% endif %}
+{% if front_config.timeout is vyos_defined %}
+{% if front_config.timeout.client is vyos_defined %}
+ timeout client {{ front_config.timeout.client }}s
+{% endif %}
+{% endif %}
{% endfor %}
{% endif %}
@@ -231,6 +254,5 @@ backend {{ back }}
{% if back_config.timeout.server is vyos_defined %}
timeout server {{ back_config.timeout.server }}s
{% endif %}
-
{% endfor %}
{% endif %}
diff --git a/data/templates/load-balancing/nftables-wlb.j2 b/data/templates/load-balancing/nftables-wlb.j2
new file mode 100644
index 000000000..b3d7c3376
--- /dev/null
+++ b/data/templates/load-balancing/nftables-wlb.j2
@@ -0,0 +1,64 @@
+#!/usr/sbin/nft -f
+
+{% if first_install is not vyos_defined %}
+delete table ip vyos_wanloadbalance
+{% endif %}
+table ip vyos_wanloadbalance {
+ chain wlb_nat_postrouting {
+ type nat hook postrouting priority srcnat - 1; policy accept;
+{% for ifname, health_conf in interface_health.items() if health_state[ifname].if_addr %}
+{% if disable_source_nat is not vyos_defined %}
+{% set state = health_state[ifname] %}
+ ct mark {{ state.mark }} counter snat to {{ state.if_addr }}
+{% endif %}
+{% endfor %}
+ }
+
+ chain wlb_mangle_prerouting {
+ type filter hook prerouting priority mangle; policy accept;
+{% for ifname, health_conf in interface_health.items() %}
+{% set state = health_state[ifname] %}
+{% if sticky_connections is vyos_defined %}
+ iifname "{{ ifname }}" ct state new ct mark set {{ state.mark }}
+{% endif %}
+{% endfor %}
+{% if rule is vyos_defined %}
+{% for rule_id, rule_conf in rule.items() %}
+{% if rule_conf.exclude is vyos_defined %}
+ {{ rule_conf | wlb_nft_rule(rule_id, exclude=True, action='return') }}
+{% else %}
+{% set limit = rule_conf.limit is vyos_defined %}
+ {{ rule_conf | wlb_nft_rule(rule_id, limit=limit, weight=True, health_state=health_state) }}
+ {{ rule_conf | wlb_nft_rule(rule_id, restore_mark=True) }}
+{% endif %}
+{% endfor %}
+{% endif %}
+ }
+
+ chain wlb_mangle_output {
+ type filter hook output priority -150; policy accept;
+{% if enable_local_traffic is vyos_defined %}
+ meta mark != 0x0 counter return
+ meta l4proto icmp counter return
+ ip saddr 127.0.0.0/8 ip daddr 127.0.0.0/8 counter return
+{% if rule is vyos_defined %}
+{% for rule_id, rule_conf in rule.items() %}
+{% if rule_conf.exclude is vyos_defined %}
+ {{ rule_conf | wlb_nft_rule(rule_id, local=True, exclude=True, action='return') }}
+{% else %}
+{% set limit = rule_conf.limit is vyos_defined %}
+ {{ rule_conf | wlb_nft_rule(rule_id, local=True, limit=limit, weight=True, health_state=health_state) }}
+ {{ rule_conf | wlb_nft_rule(rule_id, local=True, restore_mark=True) }}
+{% endif %}
+{% endfor %}
+{% endif %}
+{% endif %}
+ }
+
+{% for ifname, health_conf in interface_health.items() %}
+{% set state = health_state[ifname] %}
+ chain wlb_mangle_isp_{{ ifname }} {
+ meta mark set {{ state.mark }} ct mark set {{ state.mark }} counter accept
+ }
+{% endfor %}
+}
diff --git a/data/templates/load-balancing/wlb.conf.j2 b/data/templates/load-balancing/wlb.conf.j2
deleted file mode 100644
index 7f04d797e..000000000
--- a/data/templates/load-balancing/wlb.conf.j2
+++ /dev/null
@@ -1,134 +0,0 @@
-### Autogenerated by load-balancing_wan.py ###
-
-{% if disable_source_nat is vyos_defined %}
-disable-source-nat
-{% endif %}
-{% if enable_local_traffic is vyos_defined %}
-enable-local-traffic
-{% endif %}
-{% if sticky_connections is vyos_defined %}
-sticky-connections inbound
-{% endif %}
-{% if flush_connections is vyos_defined %}
-flush-conntrack
-{% endif %}
-{% if hook is vyos_defined %}
-hook "{{ hook }}"
-{% endif %}
-{% if interface_health is vyos_defined %}
-health {
-{% for interface, interface_config in interface_health.items() %}
- interface {{ interface }} {
-{% if interface_config.failure_count is vyos_defined %}
- failure-ct {{ interface_config.failure_count }}
-{% endif %}
-{% if interface_config.success_count is vyos_defined %}
- success-ct {{ interface_config.success_count }}
-{% endif %}
-{% if interface_config.nexthop is vyos_defined %}
- nexthop {{ interface_config.nexthop }}
-{% endif %}
-{% if interface_config.test is vyos_defined %}
-{% for test_rule, test_config in interface_config.test.items() %}
- rule {{ test_rule }} {
-{% if test_config.type is vyos_defined %}
-{% set type_translate = {'ping': 'ping', 'ttl': 'udp', 'user-defined': 'user-defined'} %}
- type {{ type_translate[test_config.type] }} {
-{% if test_config.ttl_limit is vyos_defined and test_config.type == 'ttl' %}
- ttl {{ test_config.ttl_limit }}
-{% endif %}
-{% if test_config.test_script is vyos_defined and test_config.type == 'user-defined' %}
- test-script {{ test_config.test_script }}
-{% endif %}
-{% if test_config.target is vyos_defined %}
- target {{ test_config.target }}
-{% endif %}
- resp-time {{ test_config.resp_time | int * 1000 }}
- }
-{% endif %}
- }
-{% endfor %}
-{% endif %}
- }
-{% endfor %}
-}
-{% endif %}
-
-{% if rule is vyos_defined %}
-{% for rule, rule_config in rule.items() %}
-rule {{ rule }} {
-{% if rule_config.exclude is vyos_defined %}
- exclude
-{% endif %}
-{% if rule_config.failover is vyos_defined %}
- failover
-{% endif %}
-{% if rule_config.limit is vyos_defined %}
- limit {
-{% if rule_config.limit.burst is vyos_defined %}
- burst {{ rule_config.limit.burst }}
-{% endif %}
-{% if rule_config.limit.rate is vyos_defined %}
- rate {{ rule_config.limit.rate }}
-{% endif %}
-{% if rule_config.limit.period is vyos_defined %}
- period {{ rule_config.limit.period }}
-{% endif %}
-{% if rule_config.limit.threshold is vyos_defined %}
- thresh {{ rule_config.limit.threshold }}
-{% endif %}
- }
-{% endif %}
-{% if rule_config.per_packet_balancing is vyos_defined %}
- per-packet-balancing
-{% endif %}
-{% if rule_config.protocol is vyos_defined %}
- protocol {{ rule_config.protocol }}
-{% endif %}
-{% if rule_config.destination is vyos_defined %}
- destination {
-{% if rule_config.destination.address is vyos_defined %}
- address "{{ rule_config.destination.address }}"
-{% endif %}
-{% if rule_config.destination.port is vyos_defined %}
-{% if '-' in rule_config.destination.port %}
- port-ipt "-m multiport --dports {{ rule_config.destination.port | replace('-', ':') }}"
-{% elif ',' in rule_config.destination.port %}
- port-ipt "-m multiport --dports {{ rule_config.destination.port }}"
-{% else %}
- port-ipt " --dport {{ rule_config.destination.port }}"
-{% endif %}
-{% endif %}
- }
-{% endif %}
-{% if rule_config.source is vyos_defined %}
- source {
-{% if rule_config.source.address is vyos_defined %}
- address "{{ rule_config.source.address }}"
-{% endif %}
-{% if rule_config.source.port is vyos_defined %}
-{% if '-' in rule_config.source.port %}
- port-ipt "-m multiport --sports {{ rule_config.source.port | replace('-', ':') }}"
-{% elif ',' in rule_config.destination.port %}
- port-ipt "-m multiport --sports {{ rule_config.source.port }}"
-{% else %}
- port.ipt " --sport {{ rule_config.source.port }}"
-{% endif %}
-{% endif %}
- }
-{% endif %}
-{% if rule_config.inbound_interface is vyos_defined %}
- inbound-interface {{ rule_config.inbound_interface }}
-{% endif %}
-{% if rule_config.interface is vyos_defined %}
-{% for interface, interface_config in rule_config.interface.items() %}
- interface {{ interface }} {
-{% if interface_config.weight is vyos_defined %}
- weight {{ interface_config.weight }}
-{% endif %}
- }
-{% endfor %}
-{% endif %}
-}
-{% endfor %}
-{% endif %}
diff --git a/data/templates/login/motd_vyos_nonproduction.j2 b/data/templates/login/motd_vyos_nonproduction.j2
index 3f10423ff..4b81acc5b 100644
--- a/data/templates/login/motd_vyos_nonproduction.j2
+++ b/data/templates/login/motd_vyos_nonproduction.j2
@@ -2,3 +2,4 @@
---
WARNING: This VyOS system is not a stable long-term support version and
is not intended for production use.
+
diff --git a/data/templates/prometheus/node_exporter.service.j2 b/data/templates/prometheus/node_exporter.service.j2
index 135439bd6..9a943cd75 100644
--- a/data/templates/prometheus/node_exporter.service.j2
+++ b/data/templates/prometheus/node_exporter.service.j2
@@ -9,6 +9,11 @@ After=network.target
User=node_exporter
{% endif %}
ExecStart={{ vrf_command }}/usr/sbin/node_exporter \
+{% if collectors is vyos_defined %}
+{% if collectors.textfile is vyos_defined %}
+ --collector.textfile.directory=/run/node_exporter/collector \
+{% endif %}
+{% endif %}
{% if listen_address is vyos_defined %}
{% for address in listen_address %}
--web.listen-address={{ address }}:{{ port }}
@@ -16,10 +21,6 @@ ExecStart={{ vrf_command }}/usr/sbin/node_exporter \
{% else %}
--web.listen-address=:{{ port }}
{% endif %}
-{% if collectors is vyos_defined %}
-{% if collectors.textfile is vyos_defined %}
- --collector.textfile.directory=/run/node_exporter/collector
-{% endif %}
-{% endif %}
+
[Install]
WantedBy=multi-user.target
diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2
index a83bd03ac..34f8e1f6d 100644
--- a/data/templates/router-advert/radvd.conf.j2
+++ b/data/templates/router-advert/radvd.conf.j2
@@ -57,6 +57,21 @@ interface {{ iface }} {
};
{% endfor %}
{% endif %}
+{% if iface_config.prefix is vyos_defined and "::/64" in iface_config.prefix %}
+{% if iface_config.auto_ignore is vyos_defined or iface_config.prefix | count > 1 %}
+ autoignoreprefixes {
+{% if iface_config.auto_ignore is vyos_defined %}
+{% for auto_ignore_prefix in (iface_config.auto_ignore + iface_config.prefix | list) | reject("eq", "::/64") | unique %}
+ {{ auto_ignore_prefix }};
+{% endfor %}
+{% else %}
+{% for auto_ignore_prefix in iface_config.prefix | reject("eq", "::/64") %}
+ {{ auto_ignore_prefix }};
+{% endfor %}
+{% endif %}
+ };
+{% endif %}
+{% endif %}
{% if iface_config.prefix is vyos_defined %}
{% for prefix, prefix_options in iface_config.prefix.items() %}
prefix {{ prefix }} {
diff --git a/data/templates/rsyslog/override.conf.j2 b/data/templates/rsyslog/override.conf.j2
deleted file mode 100644
index 5f6a87edf..000000000
--- a/data/templates/rsyslog/override.conf.j2
+++ /dev/null
@@ -1,11 +0,0 @@
-{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %}
-[Unit]
-StartLimitIntervalSec=0
-
-[Service]
-ExecStart=
-ExecStart={{ vrf_command }}/usr/sbin/rsyslogd -n -iNONE
-Restart=always
-RestartPreventExitStatus=
-RestartSec=10
-RuntimeDirectoryPreserve=yes
diff --git a/data/templates/rsyslog/rsyslog.conf.j2 b/data/templates/rsyslog/rsyslog.conf.j2
index 253a4bee2..6ef2afcaf 100644
--- a/data/templates/rsyslog/rsyslog.conf.j2
+++ b/data/templates/rsyslog/rsyslog.conf.j2
@@ -1,78 +1,121 @@
### Autogenerated by system_syslog.py ###
-{% if global.marker is vyos_defined %}
-$ModLoad immark
-{% if global.marker.interval is vyos_defined %}
-$MarkMessagePeriod {{ global.marker.interval }}
-{% endif %}
-{% endif %}
-{% if global.preserve_fqdn is vyos_defined %}
-$PreserveFQDN on
-{% endif %}
+#### MODULES ####
+# Load input modules for local logging and journald
-{% if global.local_host_name is vyos_defined %}
-$LocalHostName {{ global.local_host_name }}
-{% endif %}
+# Old-style log file format with low-precision timestamps
+# A modern-style logfile format with high-precision timestamps and timezone info
+# RSYSLOG_FileFormat
+module(load="builtin:omfile" Template="RSYSLOG_TraditionalFileFormat")
+module(load="imuxsock") # provides support for local system logging (collection from /dev/log unix socket)
-# We always log to /var/log/messages
-$outchannel global,/var/log/messages,262144,/usr/sbin/logrotate {{ logrotate }}
-{% if global.facility is vyos_defined %}
-{% set tmp = [] %}
-{% for facility, facility_options in global.facility.items() %}
-{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %}
-{% endfor %}
-{{ tmp | join(';') }} :omfile:$global
+# Import logs from journald, which includes kernel log messages
+module(
+ load="imjournal"
+ StateFile="/var/spool/rsyslog/imjournal.state" # Persistent state file to track the journal cursor
+ Ratelimit.Interval="0" # Disable rate limiting (set to "0" for no limit)
+ RateLimit.Burst="0"
+)
+
+###########################
+#### GLOBAL DIRECTIVES ####
+###########################
+# Log specific programs to auth.log, then stop further processing
+if (
+ $programname == "CRON" or
+ $programname == "sudo" or
+ $programname == "su"
+) then {
+ action(type="omfile" file="/var/log/auth.log")
+ stop
+}
+
+global(workDirectory="/var/spool/rsyslog")
+
+###############
+#### RULES ####
+###############
+
+# Send emergency messages to all logged-in users
+*.emerg action(type="omusrmsg" users="*")
+
+{% if marker is vyos_defined and marker.disable is not vyos_defined %}
+# Load the immark module for periodic --MARK-- message capability
+module(load="immark" interval="{{ marker.interval }}")
+{% endif %}
+{% if preserve_fqdn is vyos_defined %}
+# Preserve the fully qualified domain name (FQDN) in log messages
+global(preserveFQDN="on")
+{% if preserve_fqdn.host_name is vyos_defined and preserve_fqdn.domain_name is vyos_defined %}
+# Set the local hostname for log messages
+global(localHostname="{{ preserve_fqdn.host_name }}.{{ preserve_fqdn.domain_name }}")
+{% endif %}
{% endif %}
-{% if file is vyos_defined %}
-# File based configuration section
-{% for file_name, file_options in file.items() %}
-{% set tmp = [] %}
-$outchannel {{ file_name }},/var/log/user/{{ file_name }},{{ file_options.archive.size }},/usr/sbin/logrotate {{ logrotate }}
-{% if file_options.facility is vyos_defined %}
-{% for facility, facility_options in file_options.facility.items() %}
-{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %}
-{% endfor %}
-{% endif %}
-{{ tmp | join(';') }} :omfile:${{ file }}
-{% endfor %}
+#### GLOBAL LOGGING ####
+{% if local.facility is vyos_defined %}
+{% set tmp = [] %}
+{% if local.facility is vyos_defined %}
+{% for facility, facility_options in local.facility.items() %}
+{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %}
+{% endfor %}
+if prifilt("{{ tmp | join(',') }}") then {
+ action(
+ type="omfile"
+ file="/var/log/messages"
+ rotation.sizeLimit="524288" # 512Kib - maximum filesize before rotation
+ rotation.sizeLimitCommand="/usr/sbin/logrotate {{ logrotate }}"
+ )
+}
+{% endif %}
{% endif %}
+#### CONSOLE LOGGING ####
{% if console.facility is vyos_defined %}
-# Console logging
{% set tmp = [] %}
-{% for facility, facility_options in console.facility.items() %}
-{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %}
-{% endfor %}
-{{ tmp | join(';') }} /dev/console
+{% if console.facility is vyos_defined %}
+{% for facility, facility_options in console.facility.items() %}
+{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %}
+{% endfor %}
+if prifilt("{{ tmp | join(',') }}") then {
+ action(type="omfile" file="/dev/console")
+}
+{% endif %}
{% endif %}
-{% if host is vyos_defined %}
-# Remote logging
-{% for host_name, host_options in host.items() %}
+#### REMOTE LOGGING ####
+{% if remote is vyos_defined %}
+{% for remote_name, remote_options in remote.items() %}
{% set tmp = [] %}
-{% if host_options.facility is vyos_defined %}
-{% for facility, facility_options in host_options.facility.items() %}
-{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %}
+{% if remote_options.facility is vyos_defined %}
+{% for facility, facility_options in remote_options.facility.items() %}
+{% set _ = tmp.append(facility.replace('all', '*') ~ "." ~ facility_options.level.replace('all', 'debug')) %}
{% endfor %}
-{% endif %}
-{% if host_options.protocol is vyos_defined('tcp') %}
-{{ tmp | join(';') }} @@{{ '(o)' if host_options.format.octet_counted is vyos_defined }}{{ host_name | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.format.include_timezone is vyos_defined }}
-{% else %}
-{{ tmp | join(';') }} @{{ host_name | bracketize_ipv6 }}:{{ host_options.port }}{{ ';RSYSLOG_SyslogProtocol23Format' if host_options.format.include_timezone is vyos_defined }}
+{% set _ = tmp.sort() %}
+# Remote syslog to {{ remote_name }}
+if prifilt("{{ tmp | join(',') }}") then {
+ action(
+ type="omfwd"
+ # Remote syslog server where we send our logs to
+ target="{{ remote_name }}"
+ # Port on the remote syslog server
+ port="{{ remote_options.port }}"
+ protocol="{{ remote_options.protocol }}"
+{% if remote_options.format.include_timezone is vyos_defined %}
+ template="RSYSLOG_SyslogProtocol23Format"
+{% endif %}
+ TCP_Framing="{{ 'octet-counted' if remote_options.format.octet_counted is vyos_defined else 'traditional' }}"
+{% if remote_options.source_address is vyos_defined %}
+ Address="{{ remote_options.source_address }}"
+{% endif %}
+{% if remote_options.vrf is vyos_defined %}
+ Device="{{ remote_options.vrf }}"
+{% endif %}
+ )
+}
{% endif %}
{% endfor %}
{% endif %}
-{% if user is defined and user is not none %}
-# Log to user terminal
-{% for username, user_options in user.items() %}
-{% set tmp = [] %}
-{% if user_options.facility is vyos_defined %}
-{% for facility, facility_options in user_options.facility.items() %}
-{% set _ = tmp.append(facility.replace('all', '*') + '.' + facility_options.level.replace('all', '*')) %}
-{% endfor %}
-{% endif %}
-{{ tmp | join(';') }} :omusrmsg:{{ username }}
-{% endfor %}
-{% endif %}
+# Include all configuration files in /etc/rsyslog.d/
+include(file="/etc/rsyslog.d/*.conf")
diff --git a/data/templates/squid/squid.conf.j2 b/data/templates/squid/squid.conf.j2
index b953c8b18..4e3d702a8 100644
--- a/data/templates/squid/squid.conf.j2
+++ b/data/templates/squid/squid.conf.j2
@@ -30,6 +30,14 @@ acl BLOCKDOMAIN dstdomain {{ domain }}
{% endfor %}
http_access deny BLOCKDOMAIN
{% endif %}
+
+{% if domain_noncache is vyos_defined %}
+{% for domain in domain_noncache %}
+acl NOCACHE dstdomain {{ domain }}
+{% endfor %}
+no_cache deny NOCACHE
+{% endif %}
+
{% if authentication is vyos_defined %}
{% if authentication.children is vyos_defined %}
auth_param basic children {{ authentication.children }}
diff --git a/debian/control b/debian/control
index 76fe5c331..9b798eb97 100644
--- a/debian/control
+++ b/debian/control
@@ -8,7 +8,6 @@ Build-Depends:
fakeroot,
gcc,
iproute2,
- libvyosconfig0 (>= 0.0.7),
libzmq3-dev,
python3 (>= 3.10),
# For QA
@@ -16,6 +15,8 @@ Build-Depends:
# For generating command definitions
python3-lxml,
python3-xmltodict,
+# For generating serialization functions
+ protobuf-compiler,
# For running tests
python3-coverage,
python3-hurry.filesize,
@@ -40,8 +41,9 @@ Pre-Depends:
libpam-runtime [amd64],
libnss-tacplus [amd64],
libpam-tacplus [amd64],
- libpam-radius-auth (= 1.5.0-cl3u7) [amd64],
- libnss-mapuser (= 1.1.0-cl3u3) [amd64]
+ vyos-libpam-radius-auth,
+ vyos-libnss-mapuser,
+ tzdata (>= 2025b)
Depends:
## Fundamentals
${python3:Depends} (>= 3.10),
@@ -71,6 +73,7 @@ Depends:
python3-netifaces,
python3-paramiko,
python3-passlib,
+ python3-protobuf,
python3-pyroute2,
python3-psutil,
python3-pyhumps,
@@ -78,6 +81,7 @@ Depends:
python3-pyudev,
python3-six,
python3-tabulate,
+ python3-tomli,
python3-voluptuous,
python3-xmltodict,
python3-zmq,
@@ -116,7 +120,7 @@ Depends:
dosfstools,
grub-efi-amd64-signed [amd64],
grub-efi-arm64-bin [arm64],
- mokutil [amd64],
+ mokutil,
shim-signed [amd64],
sbsigntool [amd64],
# Image signature verification tool
@@ -124,6 +128,8 @@ Depends:
# Live filesystem tools
squashfs-tools,
fuse-overlayfs,
+# Tools for checking password strength
+ python3-cracklib,
## End installer
auditd,
iputils-arping,
@@ -190,7 +196,6 @@ Depends:
ddclient (>= 3.11.1),
# End "service dns dynamic"
# # For "service ids"
- fastnetmon [amd64],
suricata,
suricata-update,
# End "service ids"
@@ -198,14 +203,11 @@ Depends:
ndppd,
# End "service ndp-proxy"
# For "service router-advert"
- radvd,
+ radvd (>= 2.20),
# End "service route-advert"
# For "load-balancing haproxy"
haproxy,
# End "load-balancing haproxy"
-# For "load-balancing wan"
- vyatta-wanloadbalance,
-# End "load-balancing wan"
# For "service dhcp-relay"
isc-dhcp-relay,
# For "service dhcp-server"
@@ -325,6 +327,14 @@ Depends:
# iptables is only used for containers now, not the the firewall CLI
iptables,
# End container
+# For "vpp"
+ libvppinfra,
+ python3-vpp-api,
+ vpp,
+ vpp-dev,
+ vpp-plugin-core,
+ vpp-plugin-dpdk,
+# End "vpp"
## End Configuration mode
## Operational mode
# Used for hypervisor model in "run show version"
diff --git a/debian/vyos-1x.install b/debian/vyos-1x.install
index 5fcff959a..4e312a648 100644
--- a/debian/vyos-1x.install
+++ b/debian/vyos-1x.install
@@ -9,7 +9,6 @@ etc/netplug
etc/opennhrp
etc/modprobe.d
etc/ppp
-etc/rsyslog.conf
etc/securetty
etc/security
etc/skel
diff --git a/debian/vyos-1x.postinst b/debian/vyos-1x.postinst
index fde58651a..798ecaa1b 100644
--- a/debian/vyos-1x.postinst
+++ b/debian/vyos-1x.postinst
@@ -221,11 +221,9 @@ fi
# Remove unwanted daemon files from /etc
# conntackd
# pmacct
-# fastnetmon
# ntp
DELETE="/etc/logrotate.d/conntrackd.distrib /etc/init.d/conntrackd /etc/default/conntrackd
/etc/default/pmacctd /etc/pmacct
- /etc/networks_list /etc/networks_whitelist /etc/fastnetmon.conf
/etc/ntp.conf /etc/default/ssh /etc/avahi/avahi-daemon.conf /etc/avahi/hosts
/etc/powerdns /etc/default/pdns-recursor
/etc/ppp/ip-up.d/0000usepeerdns /etc/ppp/ip-down.d/0000usepeerdns"
diff --git a/debian/vyos-1x.preinst b/debian/vyos-1x.preinst
index fbfc85566..08f48cac2 100644
--- a/debian/vyos-1x.preinst
+++ b/debian/vyos-1x.preinst
@@ -5,6 +5,7 @@ dpkg-divert --package vyos-1x --add --no-rename /etc/logrotate.d/conntrackd
dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.conf
dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.bashrc
dpkg-divert --package vyos-1x --add --no-rename /etc/skel/.profile
+dpkg-divert --package vyos-1x --add --no-rename /etc/sysctl.d/80-vpp.conf
dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplugd.conf
dpkg-divert --package vyos-1x --add --no-rename /etc/netplug/netplug
dpkg-divert --package vyos-1x --add --no-rename /etc/rsyslog.d/45-frr.conf
diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in
index 04318a7c9..434bf7528 100644
--- a/interface-definitions/container.xml.in
+++ b/interface-definitions/container.xml.in
@@ -31,7 +31,7 @@
<properties>
<help>Grant individual Linux capability to container instance</help>
<completionHelp>
- <list>net-admin net-bind-service net-raw setpcap sys-admin sys-module sys-nice sys-time</list>
+ <list>net-admin net-bind-service net-raw mknod setpcap sys-admin sys-module sys-nice sys-time</list>
</completionHelp>
<valueHelp>
<format>net-admin</format>
@@ -46,6 +46,10 @@
<description>Permission to create raw network sockets</description>
</valueHelp>
<valueHelp>
+ <format>mknod</format>
+ <description>Permission to create special files</description>
+ </valueHelp>
+ <valueHelp>
<format>setpcap</format>
<description>Capability sets (from bounded or inherited set)</description>
</valueHelp>
@@ -66,11 +70,17 @@
<description>Permission to set system clock</description>
</valueHelp>
<constraint>
- <regex>(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex>
+ <regex>(net-admin|net-bind-service|net-raw|mknod|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex>
</constraint>
<multi/>
</properties>
</leafNode>
+ <leafNode name="privileged">
+ <properties>
+ <help>Grant root capabilities to the container</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<node name="sysctl">
<properties>
<help>Configure namespaced kernel parameters of the container</help>
@@ -412,6 +422,35 @@
</constraint>
</properties>
</leafNode>
+ <tagNode name="tmpfs">
+ <properties>
+ <help>Mount a tmpfs filesystem into the container</help>
+ </properties>
+ <children>
+ <leafNode name="destination">
+ <properties>
+ <help>Destination container directory</help>
+ <valueHelp>
+ <format>txt</format>
+ <description>Destination container directory</description>
+ </valueHelp>
+ </properties>
+ </leafNode>
+ <leafNode name="size">
+ <properties>
+ <help>tmpfs filesystem size in MB</help>
+ <valueHelp>
+ <format>u32:1-65536</format>
+ <description>tmpfs filesystem size in MB</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Container tmpfs size must be between 1 and 65535 MB</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
<tagNode name="volume">
<properties>
<help>Mount a volume into the container</help>
@@ -538,8 +577,75 @@
<children>
#include <include/interface/authentication.xml.i>
#include <include/generic-disable-node.xml.i>
+ <leafNode name="insecure">
+ <properties>
+ <help>Allow registry access over unencrypted HTTP or TLS connections with untrusted certificates</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <node name="mirror">
+ <properties>
+ <help>Registry mirror, use host-name|address[:port][/path]</help>
+ </properties>
+ <children>
+ <leafNode name="address">
+ <properties>
+ <help>IP address of container registry mirror</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address of container registry mirror</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>IPv6 address of container registry mirror</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ip-address"/>
+ <validator name="ipv6-link-local"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="host-name">
+ <properties>
+ <help>Hostname of container registry mirror</help>
+ <valueHelp>
+ <format>hostname</format>
+ <description>FQDN of container registry mirror</description>
+ </valueHelp>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ #include <include/port-number.xml.i>
+ <leafNode name="path">
+ <properties>
+ <help>Path of container registry mirror, optional, must be start with '/' if not empty</help>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
</children>
</tagNode>
+ <leafNode name="log-driver">
+ <properties>
+ <help>Configure container log driver</help>
+ <completionHelp>
+ <list>k8s-file journald</list>
+ </completionHelp>
+ <valueHelp>
+ <format>k8s-file</format>
+ <description>Logs to plain-text json file</description>
+ </valueHelp>
+ <valueHelp>
+ <format>journald</format>
+ <description>Logs to systemd's journal</description>
+ </valueHelp>
+ <constraint>
+ <regex>(k8s-file|journald)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
</children>
</node>
</interfaceDefinition>
diff --git a/interface-definitions/firewall.xml.in b/interface-definitions/firewall.xml.in
index e4fe9a508..7538c3cc5 100644
--- a/interface-definitions/firewall.xml.in
+++ b/interface-definitions/firewall.xml.in
@@ -138,6 +138,19 @@
</tagNode>
</children>
</node>
+ <tagNode name="remote-group">
+ <properties>
+ <help>Firewall remote-group</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
+ <constraintErrorMessage>Name of firewall group can only contain alphanumeric letters, hyphen, underscores and dot</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/url-http-https.xml.i>
+ #include <include/generic-description.xml.i>
+ </children>
+ </tagNode>
<tagNode name="interface-group">
<properties>
<help>Firewall interface-group</help>
diff --git a/interface-definitions/include/babel/redistribute-common.xml.i b/interface-definitions/include/babel/redistribute-common.xml.i
index 93efe68dd..e988cc0d0 100644
--- a/interface-definitions/include/babel/redistribute-common.xml.i
+++ b/interface-definitions/include/babel/redistribute-common.xml.i
@@ -23,6 +23,12 @@
<valueless/>
</properties>
</leafNode>
+ <leafNode name="nhrp">
+ <properties>
+ <help>Redistribute NHRP routes</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<leafNode name="openfabric">
<properties>
<help>OpenFabric Routing Protocol</help>
diff --git a/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i
new file mode 100644
index 000000000..3f6517d03
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i
@@ -0,0 +1,62 @@
+<!-- include start from bgp/afi-redistribute-common-protocols.xml.i -->
+<node name="babel">
+ <properties>
+ <help>Redistribute Babel routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<node name="connected">
+ <properties>
+ <help>Redistribute connected routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<node name="isis">
+ <properties>
+ <help>Redistribute IS-IS routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<node name="kernel">
+ <properties>
+ <help>Redistribute kernel routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<node name="nhrp">
+ <properties>
+ <help>Redistribute NHRP routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<node name="static">
+ <properties>
+ <help>Redistribute static routes into BGP</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</node>
+<tagNode name="table">
+ <properties>
+ <help>Redistribute non-main Kernel Routing Table</help>
+ <completionHelp>
+ <path>protocols static table</path>
+ </completionHelp>
+ #include <include/constraint/protocols-static-table.xml.i>
+ </properties>
+ <children>
+ #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
+ </children>
+</tagNode>
+<!-- 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
deleted file mode 100644
index 388991241..000000000
--- a/interface-definitions/include/bgp/afi-route-map-export-import.xml.i
+++ /dev/null
@@ -1,34 +0,0 @@
-<!-- include start from bgp/afi-route-map.xml.i -->
-<leafNode name="export">
- <properties>
- <help>Route-map to filter outgoing route updates</help>
- <completionHelp>
- <path>policy route-map</path>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Route map name</description>
- </valueHelp>
- <constraint>
- #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>
-</leafNode>
-<leafNode name="import">
- <properties>
- <help>Route-map to filter incoming route updates</help>
- <completionHelp>
- <path>policy route-map</path>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Route map name</description>
- </valueHelp>
- <constraint>
- #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>
-</leafNode>
-<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-export.xml.i b/interface-definitions/include/bgp/afi-route-map-export.xml.i
new file mode 100644
index 000000000..94d77caf2
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-export.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from bgp/afi-route-map-export.xml.i -->
+<leafNode name="export">
+ <properties>
+ <help>Route-map to filter outgoing route updates</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Route map name</description>
+ </valueHelp>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore-dot.xml.i>
+ </constraint>
+ <constraintErrorMessage>Route map names can only contain alphanumeric characters, hyphens, and underscores</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-import.xml.i b/interface-definitions/include/bgp/afi-route-map-import.xml.i
new file mode 100644
index 000000000..a1b154fcd
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-import.xml.i
@@ -0,0 +1,18 @@
+<!-- include start from bgp/afi-route-map-import.xml.i -->
+<leafNode name="import">
+ <properties>
+ <help>Route-map to filter incoming route updates</help>
+ <completionHelp>
+ <path>policy route-map</path>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Route map name</description>
+ </valueHelp>
+ <constraint>
+ #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>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
index e6be113c5..ac7b55af6 100644
--- a/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map-vpn.xml.i
@@ -9,7 +9,8 @@
<help>Between current address-family and VPN</help>
</properties>
<children>
- #include <include/bgp/afi-route-map-export-import.xml.i>
+ #include <include/bgp/afi-route-map-export.xml.i>
+ #include <include/bgp/afi-route-map-import.xml.i>
</children>
</node>
</children>
diff --git a/interface-definitions/include/bgp/afi-route-map-vrf.xml.i b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i
new file mode 100644
index 000000000..5c1783bda
--- /dev/null
+++ b/interface-definitions/include/bgp/afi-route-map-vrf.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from bgp/afi-route-map-vrf.xml.i -->
+<node name="route-map">
+ <properties>
+ <help>Route-map to filter route updates to/from this peer</help>
+ </properties>
+ <children>
+ <node name="vrf">
+ <properties>
+ <help>Between current address-family and VRF</help>
+ </properties>
+ <children>
+ #include <include/bgp/afi-route-map-import.xml.i>
+ </children>
+ </node>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/bgp/afi-route-map.xml.i b/interface-definitions/include/bgp/afi-route-map.xml.i
index 0b6178176..f8e1d7033 100644
--- a/interface-definitions/include/bgp/afi-route-map.xml.i
+++ b/interface-definitions/include/bgp/afi-route-map.xml.i
@@ -4,7 +4,8 @@
<help>Route-map to filter route updates to/from this peer</help>
</properties>
<children>
- #include <include/bgp/afi-route-map-export-import.xml.i>
+ #include <include/bgp/afi-route-map-export.xml.i>
+ #include <include/bgp/afi-route-map-import.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/bgp/protocol-common-config.xml.i b/interface-definitions/include/bgp/protocol-common-config.xml.i
index 4953251c5..31c8cafea 100644
--- a/interface-definitions/include/bgp/protocol-common-config.xml.i
+++ b/interface-definitions/include/bgp/protocol-common-config.xml.i
@@ -119,6 +119,7 @@
</tagNode>
#include <include/bgp/afi-rd.xml.i>
#include <include/bgp/afi-route-map-vpn.xml.i>
+ #include <include/bgp/afi-route-map-vrf.xml.i>
#include <include/bgp/afi-route-target-vpn.xml.i>
#include <include/bgp/afi-nexthop-vpn-export.xml.i>
<node name="redistribute">
@@ -126,30 +127,7 @@
<help>Redistribute routes from other protocols into BGP</help>
</properties>
<children>
- <node name="connected">
- <properties>
- <help>Redistribute connected routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <node name="isis">
- <properties>
- <help>Redistribute IS-IS routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <node name="kernel">
- <properties>
- <help>Redistribute kernel routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
+ #include <include/bgp/afi-redistribute-common-protocols.xml.i>
<node name="ospf">
<properties>
<help>Redistribute OSPF routes into BGP</help>
@@ -166,27 +144,6 @@
#include <include/bgp/afi-redistribute-metric-route-map.xml.i>
</children>
</node>
- <node name="babel">
- <properties>
- <help>Redistribute Babel routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <node name="static">
- <properties>
- <help>Redistribute static routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <leafNode name="table">
- <properties>
- <help>Redistribute non-main Kernel Routing Table</help>
- </properties>
- </leafNode>
</children>
</node>
#include <include/bgp/afi-sid.xml.i>
@@ -503,22 +460,7 @@
<help>Redistribute routes from other protocols into BGP</help>
</properties>
<children>
- <node name="connected">
- <properties>
- <help>Redistribute connected routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <node name="kernel">
- <properties>
- <help>Redistribute kernel routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
+ #include <include/bgp/afi-redistribute-common-protocols.xml.i>
<node name="ospfv3">
<properties>
<help>Redistribute OSPFv3 routes into BGP</help>
@@ -535,27 +477,6 @@
#include <include/bgp/afi-redistribute-metric-route-map.xml.i>
</children>
</node>
- <node name="babel">
- <properties>
- <help>Redistribute Babel routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <node name="static">
- <properties>
- <help>Redistribute static routes into BGP</help>
- </properties>
- <children>
- #include <include/bgp/afi-redistribute-metric-route-map.xml.i>
- </children>
- </node>
- <leafNode name="table">
- <properties>
- <help>Redistribute non-main Kernel Routing Table</help>
- </properties>
- </leafNode>
</children>
</node>
#include <include/bgp/afi-sid.xml.i>
diff --git a/interface-definitions/include/constraint/interface-name.xml.i b/interface-definitions/include/constraint/interface-name.xml.i
index 3e7c4e667..f64ea86f5 100644
--- a/interface-definitions/include/constraint/interface-name.xml.i
+++ b/interface-definitions/include/constraint/interface-name.xml.i
@@ -1,4 +1,4 @@
<!-- include start from constraint/interface-name.xml.i -->
-<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo</regex>
+<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vpptap|vpptun|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|pod-[-_a-zA-Z0-9]{1,11}|lo</regex>
<validator name="file-path --lookup-path /sys/class/net --directory"/>
<!-- include end -->
diff --git a/interface-definitions/include/constraint/protocols-static-table.xml.i b/interface-definitions/include/constraint/protocols-static-table.xml.i
new file mode 100644
index 000000000..2d8b067a4
--- /dev/null
+++ b/interface-definitions/include/constraint/protocols-static-table.xml.i
@@ -0,0 +1,9 @@
+<!-- include start from constraint/host-name.xml.i -->
+<valueHelp>
+ <format>u32:1-200</format>
+ <description>Policy route table number</description>
+</valueHelp>
+<constraint>
+ <validator name="numeric" argument="--range 1-200"/>
+</constraint>
+<!-- include end -->
diff --git a/interface-definitions/include/constraint/wireguard-keys.xml.i b/interface-definitions/include/constraint/wireguard-keys.xml.i
new file mode 100644
index 000000000..f59c86087
--- /dev/null
+++ b/interface-definitions/include/constraint/wireguard-keys.xml.i
@@ -0,0 +1,6 @@
+<!-- include start from constraint/wireguard-keys.xml.i -->
+<constraint>
+ <validator name="base64" argument="--decoded-len 32"/>
+</constraint>
+<constraintErrorMessage>Key must be Base64-encoded with 32 bytes in length</constraintErrorMessage>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp/ddns-dns-server.xml.i b/interface-definitions/include/dhcp/ddns-dns-server.xml.i
new file mode 100644
index 000000000..ba9f186d0
--- /dev/null
+++ b/interface-definitions/include/dhcp/ddns-dns-server.xml.i
@@ -0,0 +1,19 @@
+<!-- include start from dhcp/ddns-dns-server.xml.i -->
+<tagNode name="dns-server">
+ <properties>
+ <help>DNS server specification</help>
+ <valueHelp>
+ <format>u32:1-999999</format>
+ <description>Number for this DNS server</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-999999"/>
+ </constraint>
+ <constraintErrorMessage>DNS server number must be between 1 and 999999</constraintErrorMessage>
+ </properties>
+ <children>
+ #include <include/address-ipv4-ipv6-single.xml.i>
+ #include <include/port-number.xml.i>
+ </children>
+</tagNode>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp/ddns-settings.xml.i b/interface-definitions/include/dhcp/ddns-settings.xml.i
new file mode 100644
index 000000000..3e202685e
--- /dev/null
+++ b/interface-definitions/include/dhcp/ddns-settings.xml.i
@@ -0,0 +1,172 @@
+<!-- include start from dhcp/ddns-settings.xml.i -->
+<leafNode name="send-updates">
+ <properties>
+ <help>Enable or disable updates for this scope</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable updates for this scope</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable updates for this scope</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="override-client-update">
+ <properties>
+ <help>Always update both forward and reverse DNS data, regardless of the client's request</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Force update both forward and reverse DNS records</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Respect client request settings</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="override-no-update">
+ <properties>
+ <help>Perform a DDNS update, even if the client instructs the server not to</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Force DDNS updates regardless of client request</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Respect client request settings</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="replace-client-name">
+ <properties>
+ <help>Replace client name mode</help>
+ <completionHelp>
+ <list>never always when-present when-not-present</list>
+ </completionHelp>
+ <valueHelp>
+ <format>never</format>
+ <description>Use the name the client sent. If the client sent no name, do not generate
+ one</description>
+ </valueHelp>
+ <valueHelp>
+ <format>always</format>
+ <description>Replace the name the client sent. If the client sent no name, generate one
+ for the client</description>
+ </valueHelp>
+ <valueHelp>
+ <format>when-present</format>
+ <description>Replace the name the client sent. If the client sent no name, do not
+ generate one</description>
+ </valueHelp>
+ <valueHelp>
+ <format>when-not-present</format>
+ <description>Use the name the client sent. If the client sent no name, generate one for
+ the client</description>
+ </valueHelp>
+ <constraint>
+ <regex>(never|always|when-present|when-not-present)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid replace client name mode</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="generated-prefix">
+ <properties>
+ <help>The prefix used in the generation of an FQDN</help>
+ <constraint>
+ <validator name="fqdn" />
+ </constraint>
+ <constraintErrorMessage>Invalid generated prefix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="qualifying-suffix">
+ <properties>
+ <help>The suffix used when generating an FQDN, or when qualifying a partial name</help>
+ <constraint>
+ <validator name="fqdn" />
+ </constraint>
+ <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="update-on-renew">
+ <properties>
+ <help>Update DNS record on lease renew</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Update DNS record on lease renew</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Do not update DNS record on lease renew</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="conflict-resolution">
+ <properties>
+ <help>DNS conflict resolution behavior</help>
+ <completionHelp>
+ <list>enable disable</list>
+ </completionHelp>
+ <valueHelp>
+ <format>enable</format>
+ <description>Enable DNS conflict resolution</description>
+ </valueHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Disable DNS conflict resolution</description>
+ </valueHelp>
+ <constraint>
+ <regex>(enable|disable)</regex>
+ </constraint>
+ <constraintErrorMessage>Set it to either enable or disable</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="ttl-percent">
+ <properties>
+ <help>Calculate TTL of the DNS record as a percentage of the lease lifetime</help>
+ <constraint>
+ <validator name="numeric" argument="--range 1-100" />
+ </constraint>
+ <constraintErrorMessage>Invalid qualifying suffix</constraintErrorMessage>
+ </properties>
+</leafNode>
+<leafNode name="hostname-char-set">
+ <properties>
+ <help>A regular expression describing the invalid character set in the host name</help>
+ </properties>
+</leafNode>
+<leafNode name="hostname-char-replacement">
+ <properties>
+ <help>A string of zero or more characters with which to replace each invalid character in
+ the host name</help>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/dhcp/option-v4.xml.i b/interface-definitions/include/dhcp/option-v4.xml.i
index bd6fc6043..08fbcca4a 100644
--- a/interface-definitions/include/dhcp/option-v4.xml.i
+++ b/interface-definitions/include/dhcp/option-v4.xml.i
@@ -59,6 +59,18 @@
<constraintErrorMessage>DHCP client prefix length must be 0 to 32</constraintErrorMessage>
</properties>
</leafNode>
+ <leafNode name="capwap-controller">
+ <properties>
+ <help>IP address of CAPWAP access controller (Option 138)</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>CAPWAP AC controller</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name="default-router">
<properties>
<help>IP address of default router</help>
diff --git a/interface-definitions/include/dhcp/option-v6.xml.i b/interface-definitions/include/dhcp/option-v6.xml.i
index e1897f52d..202843ddf 100644
--- a/interface-definitions/include/dhcp/option-v6.xml.i
+++ b/interface-definitions/include/dhcp/option-v6.xml.i
@@ -7,6 +7,18 @@
#include <include/dhcp/captive-portal.xml.i>
#include <include/dhcp/domain-search.xml.i>
#include <include/name-server-ipv6.xml.i>
+ <leafNode name="capwap-controller">
+ <properties>
+ <help>IP address of CAPWAP access controller (Option 52)</help>
+ <valueHelp>
+ <format>ipv6</format>
+ <description>CAPWAP AC controller</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-address"/>
+ </constraint>
+ </properties>
+ </leafNode>
<leafNode name="nis-domain">
<properties>
<help>NIS domain name for client to use</help>
diff --git a/interface-definitions/include/dhcp/ping-check.xml.i b/interface-definitions/include/dhcp/ping-check.xml.i
new file mode 100644
index 000000000..a506f68e4
--- /dev/null
+++ b/interface-definitions/include/dhcp/ping-check.xml.i
@@ -0,0 +1,8 @@
+<!-- include start from dhcp/ping-check.xml.i -->
+<leafNode name="ping-check">
+ <properties>
+ <help>Sends ICMP Echo request to the address being assigned</help>
+ <valueless/>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/common-rule-ipv4.xml.i b/interface-definitions/include/firewall/common-rule-ipv4.xml.i
index 803b94b06..b67ef25dc 100644
--- a/interface-definitions/include/firewall/common-rule-ipv4.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv4.xml.i
@@ -16,6 +16,7 @@
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/source-destination-dynamic-group.xml.i>
+ #include <include/firewall/source-destination-remote-group.xml.i>
</children>
</node>
<leafNode name="jump-target">
@@ -39,6 +40,7 @@
#include <include/firewall/port.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/source-destination-dynamic-group.xml.i>
+ #include <include/firewall/source-destination-remote-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 bb176fe71..65ec415fb 100644
--- a/interface-definitions/include/firewall/common-rule-ipv6.xml.i
+++ b/interface-definitions/include/firewall/common-rule-ipv6.xml.i
@@ -16,6 +16,7 @@
#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>
+ #include <include/firewall/source-destination-remote-group.xml.i>
</children>
</node>
<leafNode name="jump-target">
@@ -39,6 +40,7 @@
#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>
+ #include <include/firewall/source-destination-remote-group.xml.i>
</children>
</node>
-<!-- include end --> \ No newline at end of file
+<!-- include end -->
diff --git a/interface-definitions/include/firewall/global-options.xml.i b/interface-definitions/include/firewall/global-options.xml.i
index 355b41fde..7393ff5c9 100644
--- a/interface-definitions/include/firewall/global-options.xml.i
+++ b/interface-definitions/include/firewall/global-options.xml.i
@@ -217,6 +217,14 @@
<help>Global firewall state-policy</help>
</properties>
<children>
+ <node name="offload">
+ <properties>
+ <help>All stateful forward traffic is offloaded to a flowtable</help>
+ </properties>
+ <children>
+ #include <include/firewall/offload-target.xml.i>
+ </children>
+ </node>
<node name="established">
<properties>
<help>Global firewall policy for packets part of an established connection</help>
diff --git a/interface-definitions/include/firewall/source-destination-remote-group.xml.i b/interface-definitions/include/firewall/source-destination-remote-group.xml.i
new file mode 100644
index 000000000..16463c8eb
--- /dev/null
+++ b/interface-definitions/include/firewall/source-destination-remote-group.xml.i
@@ -0,0 +1,17 @@
+<!-- include start from firewall/source-destination-remote-group.xml.i -->
+<node name="group">
+ <properties>
+ <help>Group</help>
+ </properties>
+ <children>
+ <leafNode name="remote-group">
+ <properties>
+ <help>Group of remote addresses</help>
+ <completionHelp>
+ <path>firewall group remote-group</path>
+ </completionHelp>
+ </properties>
+ </leafNode>
+ </children>
+</node>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/logging.xml.i b/interface-definitions/include/haproxy/logging.xml.i
index e0af54fa4..315c959bf 100644
--- a/interface-definitions/include/haproxy/logging.xml.i
+++ b/interface-definitions/include/haproxy/logging.xml.i
@@ -4,7 +4,137 @@
<help>Logging parameters</help>
</properties>
<children>
- #include <include/syslog-facility.xml.i>
+ <tagNode name="facility">
+ <properties>
+ <help>Facility for logging</help>
+ <completionHelp>
+ <list>auth cron daemon kern lpr mail news syslog user uucp local0 local1 local2 local3 local4 local5 local6 local7</list>
+ </completionHelp>
+ <constraint>
+ <regex>(auth|cron|daemon|kern|lpr|mail|news|syslog|user|uucp|local0|local1|local2|local3|local4|local5|local6|local7)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid facility type</constraintErrorMessage>
+ <valueHelp>
+ <format>auth</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>cron</format>
+ <description>Cron daemon</description>
+ </valueHelp>
+ <valueHelp>
+ <format>daemon</format>
+ <description>System daemons</description>
+ </valueHelp>
+ <valueHelp>
+ <format>kern</format>
+ <description>Kernel</description>
+ </valueHelp>
+ <valueHelp>
+ <format>lpr</format>
+ <description>Line printer spooler</description>
+ </valueHelp>
+ <valueHelp>
+ <format>mail</format>
+ <description>Mail subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>news</format>
+ <description>USENET subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>syslog</format>
+ <description>Authentication and authorization</description>
+ </valueHelp>
+ <valueHelp>
+ <format>user</format>
+ <description>Application processes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>uucp</format>
+ <description>UUCP subsystem</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local0</format>
+ <description>Local facility 0</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local1</format>
+ <description>Local facility 1</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local2</format>
+ <description>Local facility 2</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local3</format>
+ <description>Local facility 3</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local4</format>
+ <description>Local facility 4</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local5</format>
+ <description>Local facility 5</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local6</format>
+ <description>Local facility 6</description>
+ </valueHelp>
+ <valueHelp>
+ <format>local7</format>
+ <description>Local facility 7</description>
+ </valueHelp>
+ </properties>
+ <children>
+ <leafNode name="level">
+ <properties>
+ <help>Logging level</help>
+ <completionHelp>
+ <list>emerg alert crit err warning notice info debug</list>
+ </completionHelp>
+ <valueHelp>
+ <format>emerg</format>
+ <description>Emergency messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>alert</format>
+ <description>Urgent messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>crit</format>
+ <description>Critical messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>err</format>
+ <description>Error messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>warning</format>
+ <description>Warning messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>notice</format>
+ <description>Messages for further investigation</description>
+ </valueHelp>
+ <valueHelp>
+ <format>info</format>
+ <description>Informational messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>debug</format>
+ <description>Debug messages</description>
+ </valueHelp>
+ <constraint>
+ <regex>(emerg|alert|crit|err|warning|notice|info|debug)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid loglevel</constraintErrorMessage>
+ </properties>
+ <defaultValue>err</defaultValue>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout-check.xml.i b/interface-definitions/include/haproxy/timeout-check.xml.i
new file mode 100644
index 000000000..d1217fac3
--- /dev/null
+++ b/interface-definitions/include/haproxy/timeout-check.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from haproxy/timeout-check.xml.i -->
+<leafNode name="check">
+ <properties>
+ <help>Timeout in seconds for established connections</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Check timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout-client.xml.i b/interface-definitions/include/haproxy/timeout-client.xml.i
new file mode 100644
index 000000000..2250ccdef
--- /dev/null
+++ b/interface-definitions/include/haproxy/timeout-client.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from haproxy/timeout-client.xml.i -->
+<leafNode name="client">
+ <properties>
+ <help>Maximum inactivity time on the client side</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout-connect.xml.i b/interface-definitions/include/haproxy/timeout-connect.xml.i
new file mode 100644
index 000000000..da4f983af
--- /dev/null
+++ b/interface-definitions/include/haproxy/timeout-connect.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from haproxy/timeout-connect.xml.i -->
+<leafNode name="connect">
+ <properties>
+ <help>Set the maximum time to wait for a connection attempt to a server to succeed</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Connect timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout-server.xml.i b/interface-definitions/include/haproxy/timeout-server.xml.i
new file mode 100644
index 000000000..f27d415c1
--- /dev/null
+++ b/interface-definitions/include/haproxy/timeout-server.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from haproxy/timeout-server.xml.i -->
+<leafNode name="server">
+ <properties>
+ <help>Set the maximum inactivity time on the server side</help>
+ <valueHelp>
+ <format>u32:1-3600</format>
+ <description>Server timeout in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-3600"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/haproxy/timeout.xml.i b/interface-definitions/include/haproxy/timeout.xml.i
index 79e7303b1..a3a5a8a3e 100644
--- a/interface-definitions/include/haproxy/timeout.xml.i
+++ b/interface-definitions/include/haproxy/timeout.xml.i
@@ -4,42 +4,9 @@
<help>Timeout options</help>
</properties>
<children>
- <leafNode name="check">
- <properties>
- <help>Timeout in seconds for established connections</help>
- <valueHelp>
- <format>u32:1-3600</format>
- <description>Check timeout in seconds</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-3600"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="connect">
- <properties>
- <help>Set the maximum time to wait for a connection attempt to a server to succeed</help>
- <valueHelp>
- <format>u32:1-3600</format>
- <description>Connect timeout in seconds</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-3600"/>
- </constraint>
- </properties>
- </leafNode>
- <leafNode name="server">
- <properties>
- <help>Set the maximum inactivity time on the server side</help>
- <valueHelp>
- <format>u32:1-3600</format>
- <description>Server timeout in seconds</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-3600"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/haproxy/timeout-check.xml.i>
+ #include <include/haproxy/timeout-connect.xml.i>
+ #include <include/haproxy/timeout-server.xml.i>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i b/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i
new file mode 100644
index 000000000..d173dfdb8
--- /dev/null
+++ b/interface-definitions/include/interface/ipv6-address-interface-identifier.xml.i
@@ -0,0 +1,15 @@
+<!-- include start from interface/ipv6-address-interface-identifier.xml.i -->
+<leafNode name="interface-identifier">
+ <properties>
+ <help>SLAAC interface identifier</help>
+ <valueHelp>
+ <format>::h:h:h:h</format>
+ <description>Interface identifier</description>
+ </valueHelp>
+ <constraint>
+ <regex>::([0-9a-fA-F]{1,4}(:[0-9a-fA-F]{1,4}){0,3})</regex>
+ </constraint>
+ <constraintErrorMessage>Interface identifier format must start with :: and may contain up four hextets (::h:h:h:h)</constraintErrorMessage>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/interface/ipv6-address.xml.i b/interface-definitions/include/interface/ipv6-address.xml.i
deleted file mode 100644
index e1bdf02fd..000000000
--- a/interface-definitions/include/interface/ipv6-address.xml.i
+++ /dev/null
@@ -1,12 +0,0 @@
-<!-- include start from interface/ipv6-address.xml.i -->
-<node name="address">
- <properties>
- <help>IPv6 address configuration modes</help>
- </properties>
- <children>
- #include <include/interface/ipv6-address-autoconf.xml.i>
- #include <include/interface/ipv6-address-eui64.xml.i>
- #include <include/interface/ipv6-address-no-default-link-local.xml.i>
- </children>
-</node>
-<!-- include end -->
diff --git a/interface-definitions/include/interface/ipv6-options-with-nd.xml.i b/interface-definitions/include/interface/ipv6-options-with-nd.xml.i
new file mode 100644
index 000000000..5894104b3
--- /dev/null
+++ b/interface-definitions/include/interface/ipv6-options-with-nd.xml.i
@@ -0,0 +1,9 @@
+ <node name="ipv6">
+ <children>
+ <node name="address">
+ <children>
+ #include <include/interface/ipv6-address-interface-identifier.xml.i>
+ </children>
+ </node>
+ </children>
+ </node>
diff --git a/interface-definitions/include/interface/ipv6-options.xml.i b/interface-definitions/include/interface/ipv6-options.xml.i
index ec6ec64ee..f84a9f2cd 100644
--- a/interface-definitions/include/interface/ipv6-options.xml.i
+++ b/interface-definitions/include/interface/ipv6-options.xml.i
@@ -8,9 +8,18 @@
#include <include/interface/base-reachable-time.xml.i>
#include <include/interface/disable-forwarding.xml.i>
#include <include/interface/ipv6-accept-dad.xml.i>
- #include <include/interface/ipv6-address.xml.i>
#include <include/interface/ipv6-dup-addr-detect-transmits.xml.i>
#include <include/interface/source-validation.xml.i>
+ <node name="address">
+ <properties>
+ <help>IPv6 address configuration modes</help>
+ </properties>
+ <children>
+ #include <include/interface/ipv6-address-autoconf.xml.i>
+ #include <include/interface/ipv6-address-eui64.xml.i>
+ #include <include/interface/ipv6-address-no-default-link-local.xml.i>
+ </children>
+ </node>
</children>
</node>
<!-- include end -->
diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i
index 02e7ab057..65ca10207 100644
--- a/interface-definitions/include/interface/vif-s.xml.i
+++ b/interface-definitions/include/interface/vif-s.xml.i
@@ -21,6 +21,7 @@
#include <include/interface/vlan-protocol.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mirror.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
@@ -41,6 +42,7 @@
#include <include/interface/disable.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mirror.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
diff --git a/interface-definitions/include/interface/vif.xml.i b/interface-definitions/include/interface/vif.xml.i
index ec3921bf6..87f91c5ce 100644
--- a/interface-definitions/include/interface/vif.xml.i
+++ b/interface-definitions/include/interface/vif.xml.i
@@ -46,6 +46,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mirror.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
diff --git a/interface-definitions/include/ip-address.xml.i b/interface-definitions/include/ip-address.xml.i
new file mode 100644
index 000000000..6027e97ee
--- /dev/null
+++ b/interface-definitions/include/ip-address.xml.i
@@ -0,0 +1,14 @@
+<!-- include start from ip-address.xml.i -->
+<leafNode name="ip-address">
+ <properties>
+ <help>Fixed IP address of static mapping</help>
+ <valueHelp>
+ <format>ipv4</format>
+ <description>IPv4 address used in static mapping</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-address"/>
+ </constraint>
+ </properties>
+</leafNode>
+<!-- include end -->
diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i
index 35ce80be9..e0a7e62b6 100644
--- a/interface-definitions/include/isis/protocol-common-config.xml.i
+++ b/interface-definitions/include/isis/protocol-common-config.xml.i
@@ -418,6 +418,14 @@
#include <include/isis/redistribute-level-1-2.xml.i>
</children>
</node>
+ <node name="nhrp">
+ <properties>
+ <help>Redistribute NHRP routes into IS-IS</help>
+ </properties>
+ <children>
+ #include <include/isis/redistribute-level-1-2.xml.i>
+ </children>
+ </node>
<node name="ospf">
<properties>
<help>Redistribute OSPF routes into IS-IS</help>
diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i
index cef832381..f597be64e 100644
--- a/interface-definitions/include/ospf/protocol-common-config.xml.i
+++ b/interface-definitions/include/ospf/protocol-common-config.xml.i
@@ -798,6 +798,16 @@
#include <include/route-map.xml.i>
</children>
</node>
+ <node name="nhrp">
+ <properties>
+ <help>Redistribute NHRP routes</help>
+ </properties>
+ <children>
+ #include <include/ospf/metric.xml.i>
+ #include <include/ospf/metric-type.xml.i>
+ #include <include/route-map.xml.i>
+ </children>
+ </node>
<node name="rip">
<properties>
<help>Redistribute RIP routes</help>
diff --git a/interface-definitions/include/policy/community-value-list.xml.i b/interface-definitions/include/policy/community-value-list.xml.i
index 8c665c5f0..b1499440a 100644
--- a/interface-definitions/include/policy/community-value-list.xml.i
+++ b/interface-definitions/include/policy/community-value-list.xml.i
@@ -4,7 +4,6 @@
local-as
no-advertise
no-export
- internet
graceful-shutdown
accept-own
route-filter-translated-v4
@@ -35,10 +34,6 @@
<description>Well-known communities value NO_EXPORT 0xFFFFFF01</description>
</valueHelp>
<valueHelp>
- <format>internet</format>
- <description>Well-known communities value 0</description>
-</valueHelp>
-<valueHelp>
<format>graceful-shutdown</format>
<description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description>
</valueHelp>
@@ -84,7 +79,7 @@
</valueHelp>
<multi/>
<constraint>
- <regex>local-as|no-advertise|no-export|internet|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex>
+ <regex>local-as|no-advertise|no-export|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex>
<validator name="bgp-regular-community"/>
</constraint>
<!-- include end -->
diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i
index 6bed7189f..c90276151 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='5'></syntaxVersion>
+<syntaxVersion component='bgp' version='6'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/ids-version.xml.i b/interface-definitions/include/version/ids-version.xml.i
index 9133be02b..6d4e92c21 100644
--- a/interface-definitions/include/version/ids-version.xml.i
+++ b/interface-definitions/include/version/ids-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/ids-version.xml.i -->
-<syntaxVersion component='ids' version='1'></syntaxVersion>
+<syntaxVersion component='ids' version='2'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/lldp-version.xml.i b/interface-definitions/include/version/lldp-version.xml.i
index b41d80451..a7110691a 100644
--- a/interface-definitions/include/version/lldp-version.xml.i
+++ b/interface-definitions/include/version/lldp-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/lldp-version.xml.i -->
-<syntaxVersion component='lldp' version='2'></syntaxVersion>
+<syntaxVersion component='lldp' version='3'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i
index db727fea9..5c53a4032 100644
--- a/interface-definitions/include/version/policy-version.xml.i
+++ b/interface-definitions/include/version/policy-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/policy-version.xml.i -->
-<syntaxVersion component='policy' version='8'></syntaxVersion>
+<syntaxVersion component='policy' version='9'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/reverseproxy-version.xml.i b/interface-definitions/include/version/reverseproxy-version.xml.i
index 4f09f2848..71f7def1a 100644
--- a/interface-definitions/include/version/reverseproxy-version.xml.i
+++ b/interface-definitions/include/version/reverseproxy-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/reverseproxy-version.xml.i -->
-<syntaxVersion component='reverse-proxy' version='2'></syntaxVersion>
+<syntaxVersion component='reverse-proxy' version='3'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/system-version.xml.i b/interface-definitions/include/version/system-version.xml.i
index 3ecf124c7..5cdece74a 100644
--- a/interface-definitions/include/version/system-version.xml.i
+++ b/interface-definitions/include/version/system-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/system-version.xml.i -->
-<syntaxVersion component='system' version='28'></syntaxVersion>
+<syntaxVersion component='system' version='29'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/include/version/wanloadbalance-version.xml.i b/interface-definitions/include/version/wanloadbalance-version.xml.i
index 59f8729cc..34c3c76ff 100644
--- a/interface-definitions/include/version/wanloadbalance-version.xml.i
+++ b/interface-definitions/include/version/wanloadbalance-version.xml.i
@@ -1,3 +1,3 @@
<!-- include start from include/version/wanloadbalance-version.xml.i -->
-<syntaxVersion component='wanloadbalance' version='3'></syntaxVersion>
+<syntaxVersion component='wanloadbalance' version='4'></syntaxVersion>
<!-- include end -->
diff --git a/interface-definitions/interfaces_bonding.xml.in b/interface-definitions/interfaces_bonding.xml.in
index b17cad478..cdacae2d0 100644
--- a/interface-definitions/interfaces_bonding.xml.in
+++ b/interface-definitions/interfaces_bonding.xml.in
@@ -141,6 +141,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
<leafNode name="mii-mon-interval">
<properties>
diff --git a/interface-definitions/interfaces_bridge.xml.in b/interface-definitions/interfaces_bridge.xml.in
index 29dd61df5..667ae3b19 100644
--- a/interface-definitions/interfaces_bridge.xml.in
+++ b/interface-definitions/interfaces_bridge.xml.in
@@ -93,6 +93,7 @@
</node>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mirror.xml.i>
<leafNode name="enable-vlan">
diff --git a/interface-definitions/interfaces_ethernet.xml.in b/interface-definitions/interfaces_ethernet.xml.in
index b3559a626..819ceb2cb 100644
--- a/interface-definitions/interfaces_ethernet.xml.in
+++ b/interface-definitions/interfaces_ethernet.xml.in
@@ -74,6 +74,7 @@
#include <include/interface/hw-id.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
#include <include/interface/mirror.xml.i>
diff --git a/interface-definitions/interfaces_geneve.xml.in b/interface-definitions/interfaces_geneve.xml.in
index 990c5bd91..b85bd3b9e 100644
--- a/interface-definitions/interfaces_geneve.xml.in
+++ b/interface-definitions/interfaces_geneve.xml.in
@@ -21,8 +21,13 @@
#include <include/interface/disable.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mtu-1200-16000.xml.i>
+ #include <include/port-number.xml.i>
+ <leafNode name="port">
+ <defaultValue>6081</defaultValue>
+ </leafNode>
<node name="parameters">
<properties>
<help>GENEVE tunnel parameters</help>
diff --git a/interface-definitions/interfaces_l2tpv3.xml.in b/interface-definitions/interfaces_l2tpv3.xml.in
index 5f816c956..381e86bd0 100644
--- a/interface-definitions/interfaces_l2tpv3.xml.in
+++ b/interface-definitions/interfaces_l2tpv3.xml.in
@@ -55,6 +55,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/source-address-ipv4-ipv6.xml.i>
#include <include/interface/mirror.xml.i>
#include <include/interface/mtu-68-16000.xml.i>
diff --git a/interface-definitions/interfaces_macsec.xml.in b/interface-definitions/interfaces_macsec.xml.in
index d825f8262..5279a9495 100644
--- a/interface-definitions/interfaces_macsec.xml.in
+++ b/interface-definitions/interfaces_macsec.xml.in
@@ -21,6 +21,7 @@
#include <include/interface/dhcpv6-options.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mirror.xml.i>
<node name="security">
<properties>
diff --git a/interface-definitions/interfaces_openvpn.xml.in b/interface-definitions/interfaces_openvpn.xml.in
index 3c844107e..6510ed733 100644
--- a/interface-definitions/interfaces_openvpn.xml.in
+++ b/interface-definitions/interfaces_openvpn.xml.in
@@ -135,6 +135,7 @@
</node>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mirror.xml.i>
<leafNode name="hash">
<properties>
diff --git a/interface-definitions/interfaces_pppoe.xml.in b/interface-definitions/interfaces_pppoe.xml.in
index f24bc41d8..66a774e21 100644
--- a/interface-definitions/interfaces_pppoe.xml.in
+++ b/interface-definitions/interfaces_pppoe.xml.in
@@ -88,6 +88,7 @@
</properties>
<children>
#include <include/interface/ipv6-address-autoconf.xml.i>
+ #include <include/interface/ipv6-address-interface-identifier.xml.i>
</children>
</node>
#include <include/interface/adjust-mss.xml.i>
diff --git a/interface-definitions/interfaces_pseudo-ethernet.xml.in b/interface-definitions/interfaces_pseudo-ethernet.xml.in
index 031af3563..f13144bed 100644
--- a/interface-definitions/interfaces_pseudo-ethernet.xml.in
+++ b/interface-definitions/interfaces_pseudo-ethernet.xml.in
@@ -25,6 +25,7 @@
#include <include/interface/vrf.xml.i>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/source-interface-ethernet.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mirror.xml.i>
diff --git a/interface-definitions/interfaces_virtual-ethernet.xml.in b/interface-definitions/interfaces_virtual-ethernet.xml.in
index c4610feec..2dfbd50b8 100644
--- a/interface-definitions/interfaces_virtual-ethernet.xml.in
+++ b/interface-definitions/interfaces_virtual-ethernet.xml.in
@@ -21,6 +21,10 @@
#include <include/interface/dhcp-options.xml.i>
#include <include/interface/dhcpv6-options.xml.i>
#include <include/interface/disable.xml.i>
+ #include <include/interface/mtu-68-16000.xml.i>
+ <leafNode name="mtu">
+ <defaultValue>1500</defaultValue>
+ </leafNode>
#include <include/interface/netns.xml.i>
#include <include/interface/vif-s.xml.i>
#include <include/interface/vif.xml.i>
diff --git a/interface-definitions/interfaces_vxlan.xml.in b/interface-definitions/interfaces_vxlan.xml.in
index 937acb123..f4cd4fcd2 100644
--- a/interface-definitions/interfaces_vxlan.xml.in
+++ b/interface-definitions/interfaces_vxlan.xml.in
@@ -45,6 +45,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/mtu-1200-16000.xml.i>
#include <include/interface/mirror.xml.i>
diff --git a/interface-definitions/interfaces_wireguard.xml.in b/interface-definitions/interfaces_wireguard.xml.in
index 4f8b6c751..33cb5864a 100644
--- a/interface-definitions/interfaces_wireguard.xml.in
+++ b/interface-definitions/interfaces_wireguard.xml.in
@@ -56,10 +56,7 @@
<leafNode name="private-key">
<properties>
<help>Base64 encoded private key</help>
- <constraint>
- <validator name="base64"/>
- </constraint>
- <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage>
+ #include <include/constraint/wireguard-keys.xml.i>
</properties>
</leafNode>
<tagNode name="peer">
@@ -75,20 +72,14 @@
#include <include/generic-description.xml.i>
<leafNode name="public-key">
<properties>
- <help>base64 encoded public key</help>
- <constraint>
- <validator name="base64"/>
- </constraint>
- <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage>
+ <help>Base64 encoded public key</help>
+ #include <include/constraint/wireguard-keys.xml.i>
</properties>
</leafNode>
<leafNode name="preshared-key">
<properties>
- <help>base64 encoded preshared key</help>
- <constraint>
- <validator name="base64"/>
- </constraint>
- <constraintErrorMessage>Key is not base64-encoded</constraintErrorMessage>
+ <help>Base64 encoded preshared key</help>
+ #include <include/constraint/wireguard-keys.xml.i>
</properties>
</leafNode>
<leafNode name="allowed-ips">
diff --git a/interface-definitions/interfaces_wireless.xml.in b/interface-definitions/interfaces_wireless.xml.in
index 474953500..1b5356caa 100644
--- a/interface-definitions/interfaces_wireless.xml.in
+++ b/interface-definitions/interfaces_wireless.xml.in
@@ -626,6 +626,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/hw-id.xml.i>
<leafNode name="isolate-stations">
<properties>
diff --git a/interface-definitions/interfaces_wwan.xml.in b/interface-definitions/interfaces_wwan.xml.in
index 1580c3bcb..552806d4e 100644
--- a/interface-definitions/interfaces_wwan.xml.in
+++ b/interface-definitions/interfaces_wwan.xml.in
@@ -38,6 +38,7 @@
</leafNode>
#include <include/interface/ipv4-options.xml.i>
#include <include/interface/ipv6-options.xml.i>
+ #include <include/interface/ipv6-options-with-nd.xml.i>
#include <include/interface/dial-on-demand.xml.i>
#include <include/interface/redirect.xml.i>
#include <include/interface/vrf.xml.i>
diff --git a/interface-definitions/load-balancing_haproxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in
index ca089d3f0..f0f64e75a 100644
--- a/interface-definitions/load-balancing_haproxy.xml.in
+++ b/interface-definitions/load-balancing_haproxy.xml.in
@@ -4,7 +4,7 @@
<children>
<node name="haproxy" owner="${vyos_conf_scripts_dir}/load-balancing_haproxy.py">
<properties>
- <help>Configure haproxy</help>
+ <help>HAProxy TCP/HTTP Load Balancer</help>
<priority>900</priority>
</properties>
<children>
@@ -26,7 +26,7 @@
<constraintErrorMessage>Backend name must be alphanumeric and can contain hyphen and underscores</constraintErrorMessage>
<valueHelp>
<format>txt</format>
- <description>Name of haproxy backend system</description>
+ <description>HAProxy backend system name</description>
</valueHelp>
<completionHelp>
<path>load-balancing haproxy backend</path>
@@ -48,6 +48,14 @@
<valueless/>
</properties>
</leafNode>
+ <node name="timeout">
+ <properties>
+ <help>Timeout options</help>
+ </properties>
+ <children>
+ #include <include/haproxy/timeout-client.xml.i>
+ </children>
+ </node>
<node name="http-compression">
<properties>
<help>Compress HTTP responses</help>
@@ -368,6 +376,29 @@
</leafNode>
</children>
</node>
+ <node name="timeout">
+ <properties>
+ <help>Timeout options</help>
+ </properties>
+ <children>
+ #include <include/haproxy/timeout-check.xml.i>
+ <leafNode name="check">
+ <defaultValue>5</defaultValue>
+ </leafNode>
+ #include <include/haproxy/timeout-connect.xml.i>
+ <leafNode name="connect">
+ <defaultValue>10</defaultValue>
+ </leafNode>
+ #include <include/haproxy/timeout-client.xml.i>
+ <leafNode name="client">
+ <defaultValue>50</defaultValue>
+ </leafNode>
+ #include <include/haproxy/timeout-server.xml.i>
+ <leafNode name="server">
+ <defaultValue>50</defaultValue>
+ </leafNode>
+ </children>
+ </node>
#include <include/interface/vrf.xml.i>
</children>
</node>
diff --git a/interface-definitions/load-balancing_wan.xml.in b/interface-definitions/load-balancing_wan.xml.in
index 310aa0343..f80440411 100644
--- a/interface-definitions/load-balancing_wan.xml.in
+++ b/interface-definitions/load-balancing_wan.xml.in
@@ -7,7 +7,7 @@
<children>
<node name="wan" owner="${vyos_conf_scripts_dir}/load-balancing_wan.py">
<properties>
- <help>Configure Wide Area Network (WAN) load-balancing</help>
+ <help>Wide Area Network (WAN) load-balancing</help>
<priority>900</priority>
</properties>
<children>
diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in
index c59725c53..2c1babd5a 100644
--- a/interface-definitions/nat66.xml.in
+++ b/interface-definitions/nat66.xml.in
@@ -53,6 +53,7 @@
</properties>
</leafNode>
#include <include/nat-port.xml.i>
+ #include <include/firewall/source-destination-group-ipv6.xml.i>
</children>
</node>
<node name="source">
diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in
index cbab6173f..25dbf5581 100644
--- a/interface-definitions/policy.xml.in
+++ b/interface-definitions/policy.xml.in
@@ -202,7 +202,7 @@
<properties>
<help>Regular expression to match against a community-list</help>
<completionHelp>
- <list>local-AS no-advertise no-export internet graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list>
+ <list>local-AS no-advertise no-export graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list>
</completionHelp>
<valueHelp>
<format>&lt;aa:nn&gt;</format>
@@ -221,10 +221,6 @@
<description>Well-known communities value NO_EXPORT 0xFFFFFF01</description>
</valueHelp>
<valueHelp>
- <format>internet</format>
- <description>Well-known communities value 0</description>
- </valueHelp>
- <valueHelp>
<format>graceful-shutdown</format>
<description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description>
</valueHelp>
@@ -1096,6 +1092,20 @@
</constraint>
</properties>
</leafNode>
+ <leafNode name="source-vrf">
+ <properties>
+ <help>Source vrf</help>
+ #include <include/constraint/vrf.xml.i>
+ <valueHelp>
+ <format>txt</format>
+ <description>VRF instance name</description>
+ </valueHelp>
+ <completionHelp>
+ <path>vrf name</path>
+ <list>default</list>
+ </completionHelp>
+ </properties>
+ </leafNode>
#include <include/policy/tag.xml.i>
</children>
</node>
diff --git a/interface-definitions/policy_route.xml.in b/interface-definitions/policy_route.xml.in
index 9cc22540b..48f728923 100644
--- a/interface-definitions/policy_route.xml.in
+++ b/interface-definitions/policy_route.xml.in
@@ -35,6 +35,7 @@
#include <include/firewall/address-ipv6.xml.i>
#include <include/firewall/source-destination-group-ipv6.xml.i>
#include <include/firewall/port.xml.i>
+ #include <include/firewall/geoip.xml.i>
</children>
</node>
<node name="source">
@@ -45,6 +46,7 @@
#include <include/firewall/address-ipv6.xml.i>
#include <include/firewall/source-destination-group-ipv6.xml.i>
#include <include/firewall/port.xml.i>
+ #include <include/firewall/geoip.xml.i>
</children>
</node>
#include <include/policy/route-common.xml.i>
@@ -90,6 +92,7 @@
#include <include/firewall/address.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/port.xml.i>
+ #include <include/firewall/geoip.xml.i>
</children>
</node>
<node name="source">
@@ -100,6 +103,7 @@
#include <include/firewall/address.xml.i>
#include <include/firewall/source-destination-group.xml.i>
#include <include/firewall/port.xml.i>
+ #include <include/firewall/geoip.xml.i>
</children>
</node>
#include <include/policy/route-common.xml.i>
diff --git a/interface-definitions/protocols_mpls.xml.in b/interface-definitions/protocols_mpls.xml.in
index 831601fc6..fc1864f38 100644
--- a/interface-definitions/protocols_mpls.xml.in
+++ b/interface-definitions/protocols_mpls.xml.in
@@ -524,7 +524,29 @@
</node>
</children>
</node>
- #include <include/generic-interface-multi.xml.i>
+ <tagNode name="interface">
+ <properties>
+ <help>Interface</help>
+ <completionHelp>
+ <script>${vyos_completion_dir}/list_interfaces</script>
+ </completionHelp>
+ <valueHelp>
+ <format>txt</format>
+ <description>Interface name</description>
+ </valueHelp>
+ <constraint>
+ #include <include/constraint/interface-name.xml.i>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="disable-establish-hello">
+ <properties>
+ <help>Disable response to hello packet with an additional hello LDP packet</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
</children>
</node>
<node name="parameters">
diff --git a/interface-definitions/protocols_rip.xml.in b/interface-definitions/protocols_rip.xml.in
index 0edd8f2ce..745280fd7 100644
--- a/interface-definitions/protocols_rip.xml.in
+++ b/interface-definitions/protocols_rip.xml.in
@@ -209,6 +209,14 @@
#include <include/rip/redistribute.xml.i>
</children>
</node>
+ <node name="nhrp">
+ <properties>
+ <help>Redistribute NHRP routes</help>
+ </properties>
+ <children>
+ #include <include/rip/redistribute.xml.i>
+ </children>
+ </node>
<node name="ospf">
<properties>
<help>Redistribute OSPF routes</help>
diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in
index 54d69eadb..9e2e84717 100644
--- a/interface-definitions/protocols_rpki.xml.in
+++ b/interface-definitions/protocols_rpki.xml.in
@@ -42,6 +42,7 @@
</constraint>
</properties>
</leafNode>
+ #include <include/source-address-ipv4.xml.i>
<node name="ssh">
<properties>
<help>RPKI SSH connection settings</help>
diff --git a/interface-definitions/protocols_static.xml.in b/interface-definitions/protocols_static.xml.in
index d8e0ee56b..c721bb3fc 100644
--- a/interface-definitions/protocols_static.xml.in
+++ b/interface-definitions/protocols_static.xml.in
@@ -65,14 +65,8 @@
#include <include/static/static-route6.xml.i>
<tagNode name="table">
<properties>
- <help>Policy route table number</help>
- <valueHelp>
- <format>u32:1-200</format>
- <description>Policy route table number</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-200"/>
- </constraint>
+ <help>Non-main Kernel Routing Table</help>
+ #include <include/constraint/protocols-static-table.xml.i>
</properties>
<children>
<!--
diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in
index cb5f9a804..78f1cea4e 100644
--- a/interface-definitions/service_dhcp-server.xml.in
+++ b/interface-definitions/service_dhcp-server.xml.in
@@ -10,12 +10,111 @@
</properties>
<children>
#include <include/generic-disable-node.xml.i>
- <leafNode name="dynamic-dns-update">
+ <node name="dynamic-dns-update">
<properties>
<help>Dynamically update Domain Name System (RFC4702)</help>
- <valueless/>
</properties>
- </leafNode>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ <tagNode name="tsig-key">
+ <properties>
+ <help>TSIG key definition for DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="algorithm">
+ <properties>
+ <help>TSIG key algorithm</help>
+ <completionHelp>
+ <list>md5 sha1 sha224 sha256 sha384 sha512</list>
+ </completionHelp>
+ <valueHelp>
+ <format>md5</format>
+ <description>MD5 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha1</format>
+ <description>SHA1 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha224</format>
+ <description>SHA224 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha256</format>
+ <description>SHA256 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha384</format>
+ <description>SHA384 HMAC algorithm</description>
+ </valueHelp>
+ <valueHelp>
+ <format>sha512</format>
+ <description>SHA512 HMAC algorithm</description>
+ </valueHelp>
+ <constraint>
+ <regex>(md5|sha1|sha224|sha256|sha384|sha512)</regex>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key algorithm</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ <leafNode name="secret">
+ <properties>
+ <help>TSIG key secret (base64-encoded)</help>
+ <constraint>
+ <validator name="base64"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ <tagNode name="forward-domain">
+ <properties>
+ <help>Forward DNS domain name</help>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid forward DNS domain name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="key-name">
+ <properties>
+ <help>TSIG key name for forward DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/dhcp/ddns-dns-server.xml.i>
+ </children>
+ </tagNode>
+ <tagNode name="reverse-domain">
+ <properties>
+ <help>Reverse DNS domain name</help>
+ <constraint>
+ <validator name="fqdn"/>
+ </constraint>
+ <constraintErrorMessage>Invalid reverse DNS domain name</constraintErrorMessage>
+ </properties>
+ <children>
+ <leafNode name="key-name">
+ <properties>
+ <help>TSIG key name for reverse DNS updates</help>
+ <constraint>
+ #include <include/constraint/alpha-numeric-hyphen-underscore.xml.i>
+ </constraint>
+ <constraintErrorMessage>Invalid TSIG key name. May only contain letters, numbers, numbers, hyphen and underscore</constraintErrorMessage>
+ </properties>
+ </leafNode>
+ #include <include/dhcp/ddns-dns-server.xml.i>
+ </children>
+ </tagNode>
+ </children>
+ </node>
<node name="high-availability">
<properties>
<help>DHCP high availability configuration</help>
@@ -105,6 +204,14 @@
<constraintErrorMessage>Invalid shared network name. May only contain letters, numbers and .-_</constraintErrorMessage>
</properties>
<children>
+ <node name="dynamic-dns-update">
+ <properties>
+ <help>Dynamically update Domain Name System (RFC4702)</help>
+ </properties>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ </children>
+ </node>
<leafNode name="authoritative">
<properties>
<help>Option to make DHCP server authoritative for this physical network</help>
@@ -112,6 +219,7 @@
</properties>
</leafNode>
#include <include/dhcp/option-v4.xml.i>
+ #include <include/dhcp/ping-check.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
<tagNode name="subnet">
@@ -128,8 +236,17 @@
</properties>
<children>
#include <include/dhcp/option-v4.xml.i>
+ #include <include/dhcp/ping-check.xml.i>
#include <include/generic-description.xml.i>
#include <include/generic-disable-node.xml.i>
+ <node name="dynamic-dns-update">
+ <properties>
+ <help>Dynamically update Domain Name System (RFC4702)</help>
+ </properties>
+ <children>
+ #include <include/dhcp/ddns-settings.xml.i>
+ </children>
+ </node>
<leafNode name="exclude">
<properties>
<help>IP address to exclude from DHCP lease range</help>
@@ -211,18 +328,7 @@
#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>
- <help>Fixed IP address of static mapping</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address used in static mapping</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/ip-address.xml.i>
#include <include/interface/mac.xml.i>
#include <include/interface/duid.xml.i>
</children>
diff --git a/interface-definitions/service_ids_ddos-protection.xml.in b/interface-definitions/service_ids_ddos-protection.xml.in
deleted file mode 100644
index 3ef2640b3..000000000
--- a/interface-definitions/service_ids_ddos-protection.xml.in
+++ /dev/null
@@ -1,167 +0,0 @@
-<?xml version="1.0"?>
-<interfaceDefinition>
- <node name="service">
- <children>
- <node name="ids">
- <properties>
- <help>Intrusion Detection System</help>
- </properties>
- <children>
- <node name="ddos-protection" owner="${vyos_conf_scripts_dir}/service_ids_ddos-protection.py">
- <properties>
- <help>FastNetMon detection and protection parameters</help>
- <priority>731</priority>
- </properties>
- <children>
- <leafNode name="alert-script">
- <properties>
- <help>Path to fastnetmon alert script</help>
- </properties>
- </leafNode>
- <leafNode name="ban-time">
- <properties>
- <help>How long we should keep an IP in blocked state</help>
- <valueHelp>
- <format>u32:1-4294967294</format>
- <description>Time in seconds</description>
- </valueHelp>
- <constraint>
- <validator name="numeric" argument="--range 1-4294967294"/>
- </constraint>
- </properties>
- <defaultValue>1900</defaultValue>
- </leafNode>
- <leafNode name="direction">
- <properties>
- <help>Direction for processing traffic</help>
- <completionHelp>
- <list>in out</list>
- </completionHelp>
- <constraint>
- <regex>(in|out)</regex>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
- <leafNode name="excluded-network">
- <properties>
- <help>Specify IPv4 and IPv6 networks which are going to be excluded from protection</help>
- <valueHelp>
- <format>ipv4net</format>
- <description>IPv4 prefix(es) to exclude</description>
- </valueHelp>
- <valueHelp>
- <format>ipv6net</format>
- <description>IPv6 prefix(es) to exclude</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-prefix"/>
- <validator name="ipv6-prefix"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
- <leafNode name="listen-interface">
- <properties>
- <help>Listen interface for mirroring traffic</help>
- <completionHelp>
- <script>${vyos_completion_dir}/list_interfaces</script>
- </completionHelp>
- <multi/>
- </properties>
- </leafNode>
- <leafNode name="mode">
- <properties>
- <help>Traffic capture mode</help>
- <completionHelp>
- <list>mirror sflow</list>
- </completionHelp>
- <valueHelp>
- <format>mirror</format>
- <description>Listen to mirrored traffic</description>
- </valueHelp>
- <valueHelp>
- <format>sflow</format>
- <description>Capture sFlow flows</description>
- </valueHelp>
- <constraint>
- <regex>(mirror|sflow)</regex>
- </constraint>
- </properties>
- </leafNode>
- <node name="sflow">
- <properties>
- <help>Sflow settings</help>
- </properties>
- <children>
- #include <include/listen-address-ipv4-single.xml.i>
- #include <include/port-number.xml.i>
- <leafNode name="port">
- <defaultValue>6343</defaultValue>
- </leafNode>
- </children>
- </node>
- <leafNode name="network">
- <properties>
- <help>Specify IPv4 and IPv6 networks which belong to you</help>
- <valueHelp>
- <format>ipv4net</format>
- <description>Your IPv4 prefix(es)</description>
- </valueHelp>
- <valueHelp>
- <format>ipv6net</format>
- <description>Your IPv6 prefix(es)</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-prefix"/>
- <validator name="ipv6-prefix"/>
- </constraint>
- <multi/>
- </properties>
- </leafNode>
- <node name="threshold">
- <properties>
- <help>Attack limits thresholds</help>
- </properties>
- <children>
- <node name="general">
- <properties>
- <help>General threshold</help>
- </properties>
- <children>
- #include <include/ids/threshold.xml.i>
- </children>
- </node>
- <node name="tcp">
- <properties>
- <help>TCP threshold</help>
- </properties>
- <children>
- #include <include/ids/threshold.xml.i>
- </children>
- </node>
- <node name="udp">
- <properties>
- <help>UDP threshold</help>
- </properties>
- <children>
- #include <include/ids/threshold.xml.i>
- </children>
- </node>
- <node name="icmp">
- <properties>
- <help>ICMP threshold</help>
- </properties>
- <children>
- #include <include/ids/threshold.xml.i>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
- </children>
- </node>
-</interfaceDefinition>
diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in
index 6cc4471af..fe9d32bbd 100644
--- a/interface-definitions/service_ipoe-server.xml.in
+++ b/interface-definitions/service_ipoe-server.xml.in
@@ -70,18 +70,7 @@
<constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage>
</properties>
</leafNode>
- <leafNode name="static-ip">
- <properties>
- <help>Static client IP address</help>
- <valueHelp>
- <format>ipv4</format>
- <description>IPv4 address</description>
- </valueHelp>
- <constraint>
- <validator name="ipv4-address"/>
- </constraint>
- </properties>
- </leafNode>
+ #include <include/ip-address.xml.i>
</children>
</tagNode>
</children>
diff --git a/interface-definitions/service_lldp.xml.in b/interface-definitions/service_lldp.xml.in
index 51a9f9cce..a189cc13b 100644
--- a/interface-definitions/service_lldp.xml.in
+++ b/interface-definitions/service_lldp.xml.in
@@ -29,7 +29,34 @@
</constraint>
</properties>
<children>
- #include <include/generic-disable-node.xml.i>
+ <leafNode name="mode">
+ <properties>
+ <help>Set LLDP receive/transmit operation mode of this interface</help>
+ <completionHelp>
+ <list>disable rx-tx tx rx</list>
+ </completionHelp>
+ <valueHelp>
+ <format>disable</format>
+ <description>Do not process or send LLDP messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rx-tx</format>
+ <description>Send and process LLDP messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>rx</format>
+ <description>Process incoming LLDP messages</description>
+ </valueHelp>
+ <valueHelp>
+ <format>tx</format>
+ <description>Send LLDP messages</description>
+ </valueHelp>
+ <constraint>
+ <regex>(disable|rx-tx|tx|rx)</regex>
+ </constraint>
+ </properties>
+ <defaultValue>rx-tx</defaultValue>
+ </leafNode>
<node name="location">
<properties>
<help>LLDP-MED location data</help>
diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in
index 3fd33540a..7f96cdb19 100644
--- a/interface-definitions/service_router-advert.xml.in
+++ b/interface-definitions/service_router-advert.xml.in
@@ -255,6 +255,19 @@
</leafNode>
</children>
</tagNode>
+ <leafNode name="auto-ignore">
+ <properties>
+ <help>IPv6 prefix to be excluded in Router Advertisements (RAs) - use in conjunction with the ::/64 wildcard prefix</help>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>IPv6 prefix to be excluded</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
<tagNode name="prefix">
<properties>
<help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help>
diff --git a/interface-definitions/service_snmp.xml.in b/interface-definitions/service_snmp.xml.in
index f23151ef9..cc21f5b8b 100644
--- a/interface-definitions/service_snmp.xml.in
+++ b/interface-definitions/service_snmp.xml.in
@@ -304,7 +304,6 @@
</constraint>
<constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage>
</properties>
- <defaultValue></defaultValue>
</leafNode>
<tagNode name="group">
<properties>
diff --git a/interface-definitions/system_option.xml.in b/interface-definitions/system_option.xml.in
index 638ac1a3d..c0ea958a2 100644
--- a/interface-definitions/system_option.xml.in
+++ b/interface-definitions/system_option.xml.in
@@ -37,7 +37,145 @@
<help>Kernel boot parameters</help>
</properties>
<children>
- <leafNode name="disable-mitigations">
+ <node name="cpu">
+ <properties>
+ <help>CPU settings</help>
+ </properties>
+ <children>
+ <leafNode name="disable-nmi-watchdog">
+ <properties>
+ <help>Disable the NMI watchdog for detecting hard CPU lockups</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="isolate-cpus">
+ <properties>
+ <help>Isolate specified CPUs from the scheduler</help>
+ <valueHelp>
+ <format>u32:0-511</format>
+ <description>CPU core</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start-end&gt;</format>
+ <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description>
+ </valueHelp>
+ <constraint>
+ <validator name="cpu"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="nohz-full">
+ <properties>
+ <help>Enable full tickless mode for specified CPUs</help>
+ <valueHelp>
+ <format>u32:0-511</format>
+ <description>CPU core</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start-end&gt;</format>
+ <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description>
+ </valueHelp>
+ <constraint>
+ <validator name="cpu"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ <leafNode name="rcu-no-cbs">
+ <properties>
+ <help>Offload Read-Copy-Update (RCU) callback processing to specified CPUs</help>
+ <valueHelp>
+ <format>u32:0-511</format>
+ <description>CPU core</description>
+ </valueHelp>
+ <valueHelp>
+ <format>&lt;start-end&gt;</format>
+ <description>CPU core range (examples: "1", "4-7", "1,2-5,7")</description>
+ </valueHelp>
+ <constraint>
+ <validator name="cpu"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="memory">
+ <properties>
+ <help>Memory settings</help>
+ </properties>
+ <children>
+ <leafNode name="disable-numa-balancing">
+ <properties>
+ <help>Disable automatic NUMA memory balancing</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="default-hugepage-size">
+ <properties>
+ <help>Set default hugepage size (e.g., 2M, 1G)</help>
+ <completionHelp>
+ <list>2M 1G</list>
+ </completionHelp>
+ <valueHelp>
+ <format>2M</format>
+ <description>2 megabytes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1G</format>
+ <description>1 gigabyte</description>
+ </valueHelp>
+ <constraint>
+ <regex>(2M|1G)</regex>
+ </constraint>
+ </properties>
+ </leafNode>
+ <tagNode name="hugepage-size">
+ <properties>
+ <help>Set hugepage size for allocation (e.g., 2M, 1G)</help>
+ <completionHelp>
+ <list>2M 1G</list>
+ </completionHelp>
+ <valueHelp>
+ <format>2M</format>
+ <description>2 megabytes</description>
+ </valueHelp>
+ <valueHelp>
+ <format>1G</format>
+ <description>1 gigabyte</description>
+ </valueHelp>
+ <constraint>
+ <regex>(2M|1G)</regex>
+ </constraint>
+ </properties>
+ <children>
+ <leafNode name="hugepage-count">
+ <properties>
+ <help>Allocate number of hugepages for system use</help>
+ <valueHelp>
+ <format>u32</format>
+ <description>Number of hugepages</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-100000"/>
+ </constraint>
+ </properties>
+ </leafNode>
+ </children>
+ </tagNode>
+ </children>
+ </node>
+ <leafNode name="disable-hpet">
+ <properties>
+ <help>Disable High Precision Event Timer (HPET)</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-mce">
+ <properties>
+ <help>Disable Machine Check Exceptions (MCE) reporting and handling</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="disable-mitigations">
<properties>
<help>Disable all optional CPU mitigations</help>
<valueless/>
@@ -69,6 +207,18 @@
</valueHelp>
</properties>
</leafNode>
+ <leafNode name="disable-softlockup">
+ <properties>
+ <help>Disable soft lockup detector for kernel threads</help>
+ <valueless/>
+ </properties>
+ </leafNode>
+ <leafNode name="quiet">
+ <properties>
+ <help>Disable most log messages</help>
+ <valueless/>
+ </properties>
+ </leafNode>
<node name="debug">
<properties>
<help>Dynamic debugging for kernel module</help>
diff --git a/interface-definitions/system_syslog.xml.in b/interface-definitions/system_syslog.xml.in
index 0a9a00572..116cbde73 100644
--- a/interface-definitions/system_syslog.xml.in
+++ b/interface-definitions/system_syslog.xml.in
@@ -8,28 +8,17 @@
<priority>400</priority>
</properties>
<children>
- <tagNode name="user">
+ <node name="console">
<properties>
- <help>Logging to specific terminal of given user</help>
- <completionHelp>
- <path>system login user</path>
- </completionHelp>
- <valueHelp>
- <format>txt</format>
- <description>Local user account</description>
- </valueHelp>
- <constraint>
- #include <include/constraint/login-username.xml.i>
- </constraint>
- <constraintErrorMessage>illegal characters in user</constraintErrorMessage>
+ <help>Log to system console (/dev/console)</help>
</properties>
<children>
#include <include/syslog-facility.xml.i>
</children>
- </tagNode>
- <tagNode name="host">
+ </node>
+ <tagNode name="remote">
<properties>
- <help>Logging to remote host</help>
+ <help>Log to remote host</help>
<constraint>
<validator name="ip-address"/>
<validator name="fqdn"/>
@@ -49,11 +38,6 @@
</valueHelp>
</properties>
<children>
- #include <include/port-number.xml.i>
- <leafNode name="port">
- <defaultValue>514</defaultValue>
- </leafNode>
- #include <include/protocol-tcp-udp.xml.i>
#include <include/syslog-facility.xml.i>
<node name="format">
<properties>
@@ -62,98 +46,63 @@
<children>
<leafNode name="octet-counted">
<properties>
- <help>Allows for the transmission of all characters inside a syslog message</help>
+ <help>Allows for the transmission of multi-line messages (TCP only)</help>
<valueless/>
</properties>
</leafNode>
<leafNode name="include-timezone">
<properties>
- <help>Include system timezone in syslog message</help>
+ <help>Use RFC 5424 format (with RFC 3339 timestamp and timezone)</help>
<valueless/>
</properties>
</leafNode>
</children>
</node>
+ #include <include/port-number.xml.i>
+ <leafNode name="port">
+ <defaultValue>514</defaultValue>
+ </leafNode>
+ #include <include/protocol-tcp-udp.xml.i>
+ #include <include/source-address-ipv4-ipv6.xml.i>
+ #include <include/interface/vrf.xml.i>
</children>
</tagNode>
- <node name="global">
+ <node name="local">
<properties>
- <help>Logging to system standard location</help>
+ <help>Log to standard system location /var/log/messages</help>
</properties>
<children>
#include <include/syslog-facility.xml.i>
- <node name="marker">
- <properties>
- <help>mark messages sent to syslog</help>
- </properties>
- <children>
- <leafNode name="interval">
- <properties>
- <help>time interval how often a mark message is being sent in seconds</help>
- <constraint>
- <validator name="numeric" argument="--positive"/>
- </constraint>
- </properties>
- <defaultValue>1200</defaultValue>
- </leafNode>
- </children>
- </node>
- <leafNode name="preserve-fqdn">
- <properties>
- <help>uses FQDN for logging</help>
- <valueless/>
- </properties>
- </leafNode>
</children>
</node>
- <tagNode name="file">
+ <node name="marker">
<properties>
- <help>Logging to a file</help>
- <constraint>
- <regex>[a-zA-Z0-9\-_.]{1,255}</regex>
- </constraint>
- <constraintErrorMessage>illegal characters in filename or filename longer than 255 characters</constraintErrorMessage>
+ <help>Mark messages sent to syslog</help>
</properties>
<children>
- <node name="archive">
+ #include <include/generic-disable-node.xml.i>
+ <leafNode name="interval">
<properties>
- <help>Log file size and rotation characteristics</help>
+ <help>Mark message interval</help>
+ <valueHelp>
+ <format>u32:1-65535</format>
+ <description>Time in seconds</description>
+ </valueHelp>
+ <constraint>
+ <validator name="numeric" argument="--range 1-65535"/>
+ </constraint>
+ <constraintErrorMessage>Port number must be in range 1 to 65535</constraintErrorMessage>
</properties>
- <children>
- <leafNode name="file">
- <properties>
- <help>Number of saved files</help>
- <constraint>
- <regex>[0-9]+</regex>
- </constraint>
- <constraintErrorMessage>illegal characters in number of files</constraintErrorMessage>
- </properties>
- <defaultValue>5</defaultValue>
- </leafNode>
- <leafNode name="size">
- <properties>
- <help>Size of log files in kbytes</help>
- <constraint>
- <regex>[0-9]+</regex>
- </constraint>
- <constraintErrorMessage>illegal characters in size</constraintErrorMessage>
- </properties>
- <defaultValue>256</defaultValue>
- </leafNode>
- </children>
- </node>
- #include <include/syslog-facility.xml.i>
+ <defaultValue>1200</defaultValue>
+ </leafNode>
</children>
- </tagNode>
- <node name="console">
+ </node>
+ <leafNode name="preserve-fqdn">
<properties>
- <help>logging to serial console</help>
+ <help>Always include domain portion in hostname</help>
+ <valueless/>
</properties>
- <children>
- #include <include/syslog-facility.xml.i>
- </children>
- </node>
- #include <include/interface/vrf.xml.i>
+ </leafNode>
</children>
</node>
</children>
diff --git a/interface-definitions/vpn_ipsec.xml.in b/interface-definitions/vpn_ipsec.xml.in
index 0cf526fad..873a4f882 100644
--- a/interface-definitions/vpn_ipsec.xml.in
+++ b/interface-definitions/vpn_ipsec.xml.in
@@ -1244,6 +1244,63 @@
<children>
#include <include/ipsec/bind.xml.i>
#include <include/ipsec/esp-group.xml.i>
+ <node name="traffic-selector">
+ <properties>
+ <help>Traffic-selectors parameters</help>
+ </properties>
+ <children>
+ <node name="local">
+ <properties>
+ <help>Local parameters for interesting traffic</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>Local IPv4 or IPv6 prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Local IPv4 prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Local IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ <node name="remote">
+ <properties>
+ <help>Remote parameters for interesting traffic</help>
+ </properties>
+ <children>
+ <leafNode name="prefix">
+ <properties>
+ <help>Remote IPv4 or IPv6 prefix</help>
+ <valueHelp>
+ <format>ipv4net</format>
+ <description>Remote IPv4 prefix</description>
+ </valueHelp>
+ <valueHelp>
+ <format>ipv6net</format>
+ <description>Remote IPv6 prefix</description>
+ </valueHelp>
+ <constraint>
+ <validator name="ipv4-prefix"/>
+ <validator name="ipv6-prefix"/>
+ </constraint>
+ <multi/>
+ </properties>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
</children>
</node>
</children>
diff --git a/libvyosconfig b/libvyosconfig
new file mode 160000
+Subproject 1dedc69476d707718031c45b53b626da8badf86
diff --git a/op-mode-definitions/firewall.xml.in b/op-mode-definitions/firewall.xml.in
index 82e6c8668..21159eb1b 100755
--- a/op-mode-definitions/firewall.xml.in
+++ b/op-mode-definitions/firewall.xml.in
@@ -14,9 +14,16 @@
<path>firewall group address-group</path>
<path>firewall group network-group</path>
<path>firewall group port-group</path>
+ <path>firewall group domain-group</path>
+ <path>firewall group dynamic-group address-group</path>
+ <path>firewall group dynamic-group ipv6-address-group</path>
<path>firewall group interface-group</path>
<path>firewall group ipv6-address-group</path>
<path>firewall group ipv6-network-group</path>
+ <path>firewall group mac-group</path>
+ <path>firewall group network-group</path>
+ <path>firewall group port-group</path>
+ <path>firewall group remote-group</path>
</completionHelp>
</properties>
<children>
diff --git a/op-mode-definitions/generate_tech-support_archive.xml.in b/op-mode-definitions/generate_tech-support_archive.xml.in
index fc664eb90..65c93541e 100644
--- a/op-mode-definitions/generate_tech-support_archive.xml.in
+++ b/op-mode-definitions/generate_tech-support_archive.xml.in
@@ -11,12 +11,27 @@
<properties>
<help>Generate tech support archive</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip> $4.json.gz</command>
+ <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py</command>
</node>
<tagNode name="archive">
<properties>
<help>Generate tech support archive to defined location</help>
<completionHelp>
+ <list> &lt;file&gt; &lt;scp://user:passwd@host&gt; &lt;ftp://user:passwd@host&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/generate_tech-support_archive.py $4</command>
+ </tagNode>
+ <node name="machine-readable-archive">
+ <properties>
+ <help>Generate tech support archive</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/tech_support.py show --raw | gzip> $4.json.gz</command>
+ </node>
+ <tagNode name="machine-readable-archive">
+ <properties>
+ <help>Generate tech support archive to defined location</help>
+ <completionHelp>
<list> &lt;file&gt; </list>
</completionHelp>
</properties>
diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i
new file mode 100644
index 000000000..bb95ce3f5
--- /dev/null
+++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i
@@ -0,0 +1,22 @@
+<!-- included start from bgp/afi-ipv4-ipv6-vpn-rd.xml.i -->
+<tagNode name="rd">
+ <properties>
+ <help>Display routes matching the route distinguisher</help>
+ <completionHelp>
+ <list>ASN:NN IPADDRESS:NN all</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ <children>
+ <tagNode name="">
+ <properties>
+ <help>Show IP routes of specified prefix</help>
+ <completionHelp>
+ <list>&lt;x.x.x.x/x&gt; &lt;x:x:x:x:x:x:x:x/x&gt;</list>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
+ </tagNode>
+ </children>
+</tagNode>
+<!-- included end -->
diff --git a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
index f6737c8bd..a9fb6e255 100644
--- a/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
+++ b/op-mode-definitions/include/bgp/afi-ipv4-ipv6-vpn.xml.i
@@ -18,6 +18,7 @@
<children>
#include <include/bgp/afi-common.xml.i>
#include <include/bgp/afi-ipv4-ipv6-common.xml.i>
+ #include <include/bgp/afi-ipv4-ipv6-vpn-rd.xml.i>
</children>
<command>${vyos_op_scripts_dir}/vtysh_wrapper.sh $@</command>
</node>
diff --git a/op-mode-definitions/install-mok.xml.in b/op-mode-definitions/install-mok.xml.in
index 18526a354..c7e62349a 100644
--- a/op-mode-definitions/install-mok.xml.in
+++ b/op-mode-definitions/install-mok.xml.in
@@ -6,7 +6,7 @@
<properties>
<help>Install Secure Boot MOK (Machine Owner Key)</help>
</properties>
- <command>if test -f /var/lib/shim-signed/mok/MOK.der; then sudo mokutil --ignore-keyring --import /var/lib/shim-signed/mok/MOK.der; else echo "Secure Boot Machine Owner Key not found"; fi</command>
+ <command>if test -f /var/lib/shim-signed/mok/vyos-dev-2025-shim.der; then sudo mokutil --ignore-keyring --import /var/lib/shim-signed/mok/vyos-dev-2025-shim.der; else echo "Secure Boot Machine Owner Key not found"; fi</command>
</leafNode>
</children>
</node>
diff --git a/op-mode-definitions/load-balacing_haproxy.in b/op-mode-definitions/load-balancing_haproxy.xml.in
index c3d6c799b..8de7ae97f 100644
--- a/op-mode-definitions/load-balacing_haproxy.in
+++ b/op-mode-definitions/load-balancing_haproxy.xml.in
@@ -16,7 +16,7 @@
<properties>
<help>Show load-balancing haproxy</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show</command>
+ <command>sudo ${vyos_op_scripts_dir}/load-balancing_haproxy.py show</command>
</node>
</children>
</node>
diff --git a/op-mode-definitions/load-balancing_wan.xml.in b/op-mode-definitions/load-balancing_wan.xml.in
new file mode 100644
index 000000000..91c57c1f4
--- /dev/null
+++ b/op-mode-definitions/load-balancing_wan.xml.in
@@ -0,0 +1,37 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<interfaceDefinition>
+ <node name="restart">
+ <children>
+ <node name="wan-load-balance">
+ <properties>
+ <help>Restart Wide Area Network (WAN) load-balancing daemon</help>
+ </properties>
+ <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name load-balancing_wan</command>
+ </node>
+ </children>
+ </node>
+ <node name="show">
+ <children>
+ <node name="wan-load-balance">
+ <properties>
+ <help>Show Wide Area Network (WAN) load-balancing information</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_summary</command>
+ <children>
+ <node name="connection">
+ <properties>
+ <help>Show Wide Area Network (WAN) load-balancing flow</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_connection</command>
+ </node>
+ <node name="status">
+ <properties>
+ <help>Show WAN load-balancing statistics</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_status</command>
+ </node>
+ </children>
+ </node>
+ </children>
+ </node>
+</interfaceDefinition> \ No newline at end of file
diff --git a/op-mode-definitions/monitor-log.xml.in b/op-mode-definitions/monitor-log.xml.in
index b9ef8f48e..a6aa6f05e 100644
--- a/op-mode-definitions/monitor-log.xml.in
+++ b/op-mode-definitions/monitor-log.xml.in
@@ -17,19 +17,6 @@
</properties>
<command>SYSTEMD_COLORS=false grc journalctl --no-hostname --follow --boot</command>
</node>
- <node name="ids">
- <properties>
- <help>Monitor Intrusion Detection System log</help>
- </properties>
- <children>
- <leafNode name="ddos-protection">
- <properties>
- <help>Monitor last lines of DDOS protection</help>
- </properties>
- <command>journalctl --no-hostname --follow --boot --unit fastnetmon.service</command>
- </leafNode>
- </children>
- </node>
<leafNode name="certbot">
<properties>
<help>Monitor last lines of certbot log</help>
@@ -120,6 +107,12 @@
</properties>
<command>journalctl --no-hostname --boot --follow --unit frr.service</command>
</leafNode>
+ <leafNode name="haproxy">
+ <properties>
+ <help>Monitor last lines of HAProxy log</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --follow --unit haproxy.service</command>
+ </leafNode>
<leafNode name="ipoe-server">
<properties>
<help>Monitor last lines of IP over Ethernet server log</help>
diff --git a/op-mode-definitions/clear-session.xml.in b/op-mode-definitions/reset-session.xml.in
index bfafe6312..1e52e278b 100644
--- a/op-mode-definitions/clear-session.xml.in
+++ b/op-mode-definitions/reset-session.xml.in
@@ -1,6 +1,6 @@
<?xml version="1.0"?>
<interfaceDefinition>
- <node name="clear">
+ <node name="reset">
<children>
<tagNode name="session">
<properties>
diff --git a/op-mode-definitions/show-bridge.xml.in b/op-mode-definitions/show-bridge.xml.in
index 1212ab1f9..40fadac8b 100644
--- a/op-mode-definitions/show-bridge.xml.in
+++ b/op-mode-definitions/show-bridge.xml.in
@@ -7,6 +7,20 @@
<help>Show bridging information</help>
</properties>
<children>
+ <node name="spanning-tree">
+ <properties>
+ <help>View Spanning Tree info for all bridges</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/stp.py show_stp</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed Spanning Tree info for all bridges</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/stp.py show_stp --detail</command>
+ </leafNode>
+ </children>
+ </node>
<node name="vlan">
<properties>
<help>View the VLAN filter settings of the bridge</help>
@@ -44,6 +58,20 @@
</properties>
<command>bridge -c link show | grep "master $3"</command>
<children>
+ <node name="spanning-tree">
+ <properties>
+ <help>View Spanning Tree info for specified bridges</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/stp.py show_stp --ifname=$3</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show detailed Spanning Tree info for specified bridge</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/stp.py show_stp --ifname=$3 --detail</command>
+ </leafNode>
+ </children>
+ </node>
<leafNode name="mdb">
<properties>
<help>Displays the multicast group database for the bridge</help>
diff --git a/op-mode-definitions/show-interfaces.xml.in b/op-mode-definitions/show-interfaces.xml.in
index 09466647d..2d94080c7 100644
--- a/op-mode-definitions/show-interfaces.xml.in
+++ b/op-mode-definitions/show-interfaces.xml.in
@@ -26,6 +26,48 @@
</properties>
<command>${vyos_op_scripts_dir}/interfaces.py show_summary</command>
</leafNode>
+ <tagNode name="kernel">
+ <properties>
+ <completionHelp>
+ <script>ip -j link show | jq -r '.[].ifname'</script>
+ </completionHelp>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show system interface in JSON format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4 --detail</command>
+ </leafNode>
+ <leafNode name="json">
+ <properties>
+ <help>Show system interface in JSON format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --intf-name=$4 --raw</command>
+ </leafNode>
+ </children>
+ </tagNode>
+ <node name="kernel">
+ <properties>
+ <help>Show all interfaces on this system</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel</command>
+ <children>
+ <leafNode name="detail">
+ <properties>
+ <help>Show system interface in JSON format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --detail</command>
+ </leafNode>
+ <leafNode name="json">
+ <properties>
+ <help>Show all interfaces in JSON format</help>
+ </properties>
+ <command>${vyos_op_scripts_dir}/interfaces.py show_kernel --raw</command>
+ </leafNode>
+ </children>
+ </node>
</children>
</node>
</children>
diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in
index 5ee7c973f..c2bc03910 100755
--- a/op-mode-definitions/show-log.xml.in
+++ b/op-mode-definitions/show-log.xml.in
@@ -50,6 +50,39 @@
</properties>
<command>cat $(printf "%s\n" /var/log/messages* | sort -nr) | grep -e heartbeat -e cl_status -e mach_down -e ha_log</command>
</leafNode>
+ <node name="conntrack">
+ <properties>
+ <help>Show log for conntrack events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW|UPDATE|DESTROY)\]'</command>
+ <children>
+ <node name="event">
+ <properties>
+ <help>Show log for conntrack events</help>
+ </properties>
+ <children>
+ <leafNode name="new">
+ <properties>
+ <help>Show log for conntrack events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(NEW)\]'</command>
+ </leafNode>
+ <leafNode name="update">
+ <properties>
+ <help>Show log for conntrack events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(UPDATE)\]'</command>
+ </leafNode>
+ <leafNode name="destroy">
+ <properties>
+ <help>Show log for Conntrack Events</help>
+ </properties>
+ <command>journalctl --no-hostname --boot -t vyos-conntrack-logger --grep='\[(DESTROY)\]'</command>
+ </leafNode>
+ </children>
+ </node>
+ </children>
+ </node>
<leafNode name="conntrack-sync">
<properties>
<help>Show log for Conntrack-sync</help>
@@ -62,19 +95,6 @@
</properties>
<command>journalctl --no-hostname --boot --unit conserver-server.service</command>
</leafNode>
- <node name="ids">
- <properties>
- <help>Show log for for Intrusion Detection System</help>
- </properties>
- <children>
- <leafNode name="ddos-protection">
- <properties>
- <help>Show log for DDOS protection</help>
- </properties>
- <command>journalctl --no-hostname --boot --unit fastnetmon.service</command>
- </leafNode>
- </children>
- </node>
<node name="dhcp">
<properties>
<help>Show log for Dynamic Host Control Protocol (DHCP)</help>
@@ -139,7 +159,7 @@
<properties>
<help>Show log for Firewall</help>
</properties>
- <command>journalctl --no-hostname --boot -k | egrep "(ipv[46]|bri)-(FWD|INP|OUT|NAM)"</command>
+ <command>journalctl --no-hostname --boot -k --grep='(ipv[46]|bri)-(FWD|INP|OUT|NAM)|STATE-POLICY'</command>
<children>
<node name="bridge">
<properties>
@@ -539,6 +559,12 @@
</properties>
<command>journalctl --no-hostname --boot --unit frr.service</command>
</leafNode>
+ <leafNode name="haproxy">
+ <properties>
+ <help>Show log for HAProxy</help>
+ </properties>
+ <command>journalctl --no-hostname --boot --unit haproxy.service</command>
+ </leafNode>
<leafNode name="https">
<properties>
<help>Show log for HTTPs</help>
diff --git a/op-mode-definitions/system-image.xml.in b/op-mode-definitions/system-image.xml.in
index 44b055be6..847029dcd 100644
--- a/op-mode-definitions/system-image.xml.in
+++ b/op-mode-definitions/system-image.xml.in
@@ -193,7 +193,7 @@
<properties>
<help>Show installed VyOS images</help>
</properties>
- <command>sudo ${vyos_op_scripts_dir}/image_info.py show_images_summary</command>
+ <command>${vyos_op_scripts_dir}/image_info.py show_images_summary</command>
<children>
<node name="details">
<properties>
diff --git a/python/setup.py b/python/setup.py
index 2d614e724..571b956ee 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -1,5 +1,14 @@
import os
+import sys
+import subprocess
from setuptools import setup
+from setuptools.command.build_py import build_py
+
+sys.path.append('./vyos')
+from defaults import directories
+
+def desc_out(f):
+ return os.path.splitext(f)[0] + '.desc'
def packages(directory):
return [
@@ -8,6 +17,43 @@ def packages(directory):
if os.path.isfile(os.path.join(_[0], '__init__.py'))
]
+
+class GenerateProto(build_py):
+ ver = os.environ.get('OCAML_VERSION')
+ if ver:
+ proto_path = f'/opt/opam/{ver}/share/vyconf'
+ else:
+ proto_path = directories['proto_path']
+
+ def run(self):
+ # find all .proto files in vyconf proto_path
+ proto_files = []
+ for _, _, files in os.walk(self.proto_path):
+ for file in files:
+ if file.endswith('.proto'):
+ proto_files.append(file)
+
+ # compile each .proto file to Python
+ for proto_file in proto_files:
+ subprocess.check_call(
+ [
+ 'protoc',
+ '--python_out=vyos/proto',
+ f'--proto_path={self.proto_path}/',
+ f'--descriptor_set_out=vyos/proto/{desc_out(proto_file)}',
+ proto_file,
+ ]
+ )
+ subprocess.check_call(
+ [
+ 'vyos/proto/generate_dataclass.py',
+ 'vyos/proto/vyconf.desc',
+ '--out-dir=vyos/proto',
+ ]
+ )
+
+ build_py.run(self)
+
setup(
name = "vyos",
version = "1.3.0",
@@ -29,4 +75,7 @@ setup(
"config-mgmt = vyos.config_mgmt:run",
],
},
+ cmdclass={
+ 'build_py': GenerateProto,
+ },
)
diff --git a/python/vyos/base.py b/python/vyos/base.py
index ca96d96ce..3173ddc20 100644
--- a/python/vyos/base.py
+++ b/python/vyos/base.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2022 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2018-2025 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,8 +15,7 @@
from textwrap import fill
-
-class BaseWarning:
+class UserMessage:
def __init__(self, header, message, **kwargs):
self.message = message
self.kwargs = kwargs
@@ -33,7 +32,6 @@ class BaseWarning:
messages = self.message.split('\n')
isfirstmessage = True
initial_indent = self.textinitindent
- print('')
for mes in messages:
mes = fill(mes, initial_indent=initial_indent,
subsequent_indent=self.standardindent, **self.kwargs)
@@ -44,17 +42,24 @@ class BaseWarning:
print('', flush=True)
+class Message():
+ def __init__(self, message, **kwargs):
+ self.Message = UserMessage('', message, **kwargs)
+ self.Message.print()
+
class Warning():
def __init__(self, message, **kwargs):
- self.BaseWarn = BaseWarning('WARNING: ', message, **kwargs)
- self.BaseWarn.print()
+ print('')
+ self.UserMessage = UserMessage('WARNING: ', message, **kwargs)
+ self.UserMessage.print()
class DeprecationWarning():
def __init__(self, message, **kwargs):
# Reformat the message and trim it to 72 characters in length
- self.BaseWarn = BaseWarning('DEPRECATION WARNING: ', message, **kwargs)
- self.BaseWarn.print()
+ print('')
+ self.UserMessage = UserMessage('DEPRECATION WARNING: ', message, **kwargs)
+ self.UserMessage.print()
class ConfigError(Exception):
diff --git a/python/vyos/component_version.py b/python/vyos/component_version.py
index 94215531d..81d986658 100644
--- a/python/vyos/component_version.py
+++ b/python/vyos/component_version.py
@@ -49,7 +49,9 @@ DEFAULT_CONFIG_PATH = os.path.join(directories['config'], 'config.boot')
REGEX_WARN_VYOS = r'(// Warning: Do not remove the following line.)'
REGEX_WARN_VYATTA = r'(/\* Warning: Do not remove the following line. \*/)'
REGEX_COMPONENT_VERSION_VYOS = r'// vyos-config-version:\s+"([\w@:-]+)"\s*'
-REGEX_COMPONENT_VERSION_VYATTA = r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+REGEX_COMPONENT_VERSION_VYATTA = (
+ r'/\* === vyatta-config-version:\s+"([\w@:-]+)"\s+=== \*/'
+)
REGEX_RELEASE_VERSION_VYOS = r'// Release version:\s+(\S*)\s*'
REGEX_RELEASE_VERSION_VYATTA = r'/\* Release version:\s+(\S*)\s*\*/'
@@ -62,16 +64,31 @@ CONFIG_FILE_VERSION = """\
warn_filter_vyos = re.compile(REGEX_WARN_VYOS)
warn_filter_vyatta = re.compile(REGEX_WARN_VYATTA)
-regex_filter = { 'vyos': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYOS),
- re.compile(REGEX_RELEASE_VERSION_VYOS)])),
- 'vyatta': dict(zip(['component', 'release'],
- [re.compile(REGEX_COMPONENT_VERSION_VYATTA),
- re.compile(REGEX_RELEASE_VERSION_VYATTA)])) }
+regex_filter = {
+ 'vyos': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYOS),
+ re.compile(REGEX_RELEASE_VERSION_VYOS),
+ ],
+ )
+ ),
+ 'vyatta': dict(
+ zip(
+ ['component', 'release'],
+ [
+ re.compile(REGEX_COMPONENT_VERSION_VYATTA),
+ re.compile(REGEX_RELEASE_VERSION_VYATTA),
+ ],
+ )
+ ),
+}
+
@dataclass
class VersionInfo:
- component: Optional[dict[str,int]] = None
+ component: Optional[dict[str, int]] = None
release: str = get_version()
vintage: str = 'vyos'
config_body: Optional[str] = None
@@ -84,8 +101,9 @@ class VersionInfo:
return bool(self.config_body is None)
def update_footer(self):
- f = CONFIG_FILE_VERSION.format(component_to_string(self.component),
- self.release)
+ f = CONFIG_FILE_VERSION.format(
+ component_to_string(self.component), self.release
+ )
self.footer_lines = f.splitlines()
def update_syntax(self):
@@ -121,13 +139,16 @@ class VersionInfo:
except Exception as e:
raise ValueError(e) from e
+
def component_to_string(component: dict) -> str:
- l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])]
+ l = [f'{k}@{v}' for k, v in sorted(component.items(), key=lambda x: x[0])] # noqa: E741
return ':'.join(l)
+
def component_from_string(string: str) -> dict:
return {k: int(v) for k, v in re.findall(r'([\w,-]+)@(\d+)', string)}
+
def version_info_from_file(config_file) -> VersionInfo:
"""Return config file component and release version info."""
version_info = VersionInfo()
@@ -166,27 +187,27 @@ def version_info_from_file(config_file) -> VersionInfo:
return version_info
+
def version_info_from_system() -> VersionInfo:
"""Return system component and release version info."""
d = component_version()
sort_d = dict(sorted(d.items(), key=lambda x: x[0]))
- version_info = VersionInfo(
- component = sort_d,
- release = get_version(),
- vintage = 'vyos'
- )
+ version_info = VersionInfo(component=sort_d, release=get_version(), vintage='vyos')
return version_info
+
def version_info_copy(v: VersionInfo) -> VersionInfo:
"""Make a copy of dataclass."""
return replace(v)
+
def version_info_prune_component(x: VersionInfo, y: VersionInfo) -> VersionInfo:
"""In place pruning of component keys of x not in y."""
if x.component is None or y.component is None:
return
- x.component = { k: v for k,v in x.component.items() if k in y.component }
+ x.component = {k: v for k, v in x.component.items() if k in y.component}
+
def add_system_version(config_str: str = None, out_file: str = None):
"""Wrap config string with system version and write to out_file.
@@ -202,3 +223,11 @@ def add_system_version(config_str: str = None, out_file: str = None):
version_info.write(out_file)
else:
sys.stdout.write(version_info.write_string())
+
+
+def append_system_version(file: str):
+ """Append system version data to existing file"""
+ version_info = version_info_from_system()
+ version_info.update_footer()
+ with open(file, 'a') as f:
+ f.write(version_info.write_string())
diff --git a/python/vyos/config.py b/python/vyos/config.py
index 1fab46761..546eeceab 100644
--- a/python/vyos/config.py
+++ b/python/vyos/config.py
@@ -149,6 +149,18 @@ class Config(object):
return self._running_config
return self._session_config
+ def get_bool_attr(self, attr) -> bool:
+ if not hasattr(self, attr):
+ return False
+ else:
+ tmp = getattr(self, attr)
+ if not isinstance(tmp, bool):
+ return False
+ return tmp
+
+ def set_bool_attr(self, attr, val):
+ setattr(self, attr, val)
+
def _make_path(self, path):
# Backwards-compatibility stuff: original implementation used string paths
# libvyosconfig paths are lists, but since node names cannot contain whitespace,
diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py
index 1c2b70fdf..dd8910afb 100644
--- a/python/vyos/config_mgmt.py
+++ b/python/vyos/config_mgmt.py
@@ -287,7 +287,7 @@ Proceed ?"""
# commits under commit-confirm are not added to revision list unless
# confirmed, hence a soft revert is to revision 0
- revert_ct = self._get_config_tree_revision(0)
+ revert_ct = self.get_config_tree_revision(0)
message = '[commit-confirm] Reverting to previous config now'
os.system('wall -n ' + message)
@@ -351,7 +351,7 @@ Proceed ?"""
)
return msg, 1
- rollback_ct = self._get_config_tree_revision(rev)
+ rollback_ct = self.get_config_tree_revision(rev)
try:
load(rollback_ct, switch='explicit')
print('Rollback diff has been applied.')
@@ -382,7 +382,7 @@ Proceed ?"""
if rev1 is not None:
if not self._check_revision_number(rev1):
return f'Invalid revision number {rev1}', 1
- ct1 = self._get_config_tree_revision(rev1)
+ ct1 = self.get_config_tree_revision(rev1)
ct2 = self.working_config
msg = f'No changes between working and revision {rev1} configurations.\n'
if rev2 is not None:
@@ -390,7 +390,7 @@ Proceed ?"""
return f'Invalid revision number {rev2}', 1
# compare older to newer
ct2 = ct1
- ct1 = self._get_config_tree_revision(rev2)
+ ct1 = self.get_config_tree_revision(rev2)
msg = f'No changes between revisions {rev2} and {rev1} configurations.\n'
out = ''
@@ -575,7 +575,7 @@ Proceed ?"""
r = f.read().decode()
return r
- def _get_config_tree_revision(self, rev: int):
+ def get_config_tree_revision(self, rev: int):
c = self._get_file_revision(rev)
return ConfigTree(c)
diff --git a/python/vyos/configdep.py b/python/vyos/configdep.py
index cf7c9d543..747af8dbe 100644
--- a/python/vyos/configdep.py
+++ b/python/vyos/configdep.py
@@ -102,11 +102,16 @@ def run_config_mode_script(target: str, config: 'Config'):
mod = load_as_module(name, path)
config.set_level([])
+ dry_run = config.get_bool_attr('dry_run')
try:
c = mod.get_config(config)
mod.verify(c)
- mod.generate(c)
- mod.apply(c)
+ if not dry_run:
+ mod.generate(c)
+ mod.apply(c)
+ else:
+ if hasattr(mod, 'call_dependents'):
+ mod.call_dependents()
except (VyOSError, ConfigError) as e:
raise ConfigError(str(e)) from e
diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py
index 5a353b110..ff0a15933 100644
--- a/python/vyos/configdict.py
+++ b/python/vyos/configdict.py
@@ -491,10 +491,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'dhcp-options'])
if dhcp: dict.update({'dhcp_options_changed' : {}})
-
- # Changine interface VRF assignemnts require a DHCP restart, too
- dhcp = is_node_changed(config, base + [ifname, 'vrf'])
- if dhcp: dict.update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'dhcpv6-options'])
+ if dhcpv6: dict.update({'dhcpv6_options_changed' : {}})
# Some interfaces come with a source_interface which must also not be part
# of any other bond or bridge interface as it is exclusivly assigned as the
@@ -519,6 +517,14 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
else:
dict['ipv6']['address'].update({'eui64_old': eui64})
+ interface_identifier = leaf_node_changed(config, base + [ifname, 'ipv6', 'address', 'interface-identifier'])
+ if interface_identifier:
+ tmp = dict_search('ipv6.address', dict)
+ if not tmp:
+ dict.update({'ipv6': {'address': {'interface_identifier_old': interface_identifier}}})
+ else:
+ dict['ipv6']['address'].update({'interface_identifier_old': interface_identifier})
+
for vif, vif_config in dict.get('vif', {}).items():
# Add subinterface name to dictionary
dict['vif'][vif].update({'ifname' : f'{ifname}.{vif}'})
@@ -543,6 +549,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcp-options'])
if dhcp: dict['vif'][vif].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif', vif, 'dhcpv6-options'])
+ if dhcpv6: dict['vif'][vif].update({'dhcpv6_options_changed' : {}})
for vif_s, vif_s_config in dict.get('vif_s', {}).items():
# Add subinterface name to dictionary
@@ -569,6 +577,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcp-options'])
if dhcp: dict['vif_s'][vif_s].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'dhcpv6-options'])
+ if dhcpv6: dict['vif_s'][vif_s].update({'dhcpv6_options_changed' : {}})
for vif_c, vif_c_config in vif_s_config.get('vif_c', {}).items():
# Add subinterface name to dictionary
@@ -597,6 +607,8 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True, with_pk
# Check if any DHCP options changed which require a client restat
dhcp = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcp-options'])
if dhcp: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcp_options_changed' : {}})
+ dhcpv6 = is_node_changed(config, base + [ifname, 'vif-s', vif_s, 'vif-c', vif_c, 'dhcpv6-options'])
+ if dhcpv6: dict['vif_s'][vif_s]['vif_c'][vif_c].update({'dhcpv6_options_changed' : {}})
# Check vif, vif-s/vif-c VLAN interfaces for removal
dict = get_removed_vlans(config, base + [ifname], dict)
@@ -622,6 +634,23 @@ def get_vlan_ids(interface):
return vlan_ids
+def get_vlans_ids_and_range(interface):
+ vlan_ids = set()
+
+ vlan_filter_status = json.loads(cmd(f'bridge -j -d vlan show dev {interface}'))
+
+ if vlan_filter_status is not None:
+ for interface_status in vlan_filter_status:
+ for vlan_entry in interface_status.get("vlans", []):
+ start = vlan_entry["vlan"]
+ end = vlan_entry.get("vlanEnd")
+ if end:
+ vlan_ids.add(f"{start}-{end}")
+ else:
+ vlan_ids.add(str(start))
+
+ return vlan_ids
+
def get_accel_dict(config, base, chap_secrets, with_pki=False):
"""
Common utility function to retrieve and mangle the Accel-PPP configuration
diff --git a/python/vyos/configsession.py b/python/vyos/configsession.py
index dd3ad1e3d..a3be29881 100644
--- a/python/vyos/configsession.py
+++ b/python/vyos/configsession.py
@@ -21,10 +21,14 @@ import subprocess
from vyos.defaults import directories
from vyos.utils.process import is_systemd_service_running
from vyos.utils.dict import dict_to_paths
+from vyos.utils.boot import boot_configuration_complete
+from vyos.vyconf_session import VyconfSession
+
+vyconf_backend = False
CLI_SHELL_API = '/bin/cli-shell-api'
-SET = '/usr/libexec/vyos/vyconf/vy_set'
-DELETE = '/usr/libexec/vyos/vyconf/vy_delete'
+SET = '/opt/vyatta/sbin/my_set'
+DELETE = '/opt/vyatta/sbin/my_delete'
COMMENT = '/opt/vyatta/sbin/my_comment'
COMMIT = '/opt/vyatta/sbin/my_commit'
DISCARD = '/opt/vyatta/sbin/my_discard'
@@ -165,6 +169,11 @@ class ConfigSession(object):
self.__run_command([CLI_SHELL_API, 'setupSession'])
+ if vyconf_backend and boot_configuration_complete():
+ self._vyconf_session = VyconfSession(on_error=ConfigSessionError)
+ else:
+ self._vyconf_session = None
+
def __del__(self):
try:
output = (
@@ -209,7 +218,10 @@ class ConfigSession(object):
value = []
else:
value = [value]
- self.__run_command([SET] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([SET] + path + value)
+ else:
+ self._vyconf_session.set(path + value)
def set_section(self, path: list, d: dict):
try:
@@ -223,7 +235,10 @@ class ConfigSession(object):
value = []
else:
value = [value]
- self.__run_command([DELETE] + path + value)
+ if self._vyconf_session is None:
+ self.__run_command([DELETE] + path + value)
+ else:
+ self._vyconf_session.delete(path + value)
def load_section(self, path: list, d: dict):
try:
@@ -261,20 +276,34 @@ class ConfigSession(object):
self.__run_command([COMMENT] + path + value)
def commit(self):
- out = self.__run_command([COMMIT])
+ if self._vyconf_session is None:
+ out = self.__run_command([COMMIT])
+ else:
+ out, _ = self._vyconf_session.commit()
+
return out
def discard(self):
- self.__run_command([DISCARD])
+ if self._vyconf_session is None:
+ self.__run_command([DISCARD])
+ else:
+ out, _ = self._vyconf_session.discard()
def show_config(self, path, format='raw'):
- config_data = self.__run_command(SHOW_CONFIG + path)
+ if self._vyconf_session is None:
+ config_data = self.__run_command(SHOW_CONFIG + path)
+ else:
+ config_data, _ = self._vyconf_session.show_config()
if format == 'raw':
return config_data
def load_config(self, file_path):
- out = self.__run_command(LOAD_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(LOAD_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.load_config(file=file_path)
+
return out
def load_explicit(self, file_path):
@@ -287,11 +316,21 @@ class ConfigSession(object):
raise ConfigSessionError(e) from e
def migrate_and_load_config(self, file_path):
- out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(MIGRATE_LOAD_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.load_config(file=file_path, migrate=True)
+
return out
def save_config(self, file_path):
- out = self.__run_command(SAVE_CONFIG + [file_path])
+ if self._vyconf_session is None:
+ out = self.__run_command(SAVE_CONFIG + [file_path])
+ else:
+ out, _ = self._vyconf_session.save_config(
+ file=file_path, append_version=True
+ )
+
return out
def install_image(self, url):
diff --git a/python/vyos/configsource.py b/python/vyos/configsource.py
index 59e5ac8a1..65cef5333 100644
--- a/python/vyos/configsource.py
+++ b/python/vyos/configsource.py
@@ -319,3 +319,13 @@ class ConfigSourceString(ConfigSource):
self._session_config = ConfigTree(session_config_text) if session_config_text else None
except ValueError:
raise ConfigSourceError(f"Init error in {type(self)}")
+
+class ConfigSourceCache(ConfigSource):
+ def __init__(self, running_config_cache=None, session_config_cache=None):
+ super().__init__()
+
+ try:
+ self._running_config = ConfigTree(internal=running_config_cache) if running_config_cache else None
+ self._session_config = ConfigTree(internal=session_config_cache) if session_config_cache else None
+ except ValueError:
+ raise ConfigSourceError(f"Init error in {type(self)}")
diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py
index 8d27a7e46..ff40fbad0 100644
--- a/python/vyos/configtree.py
+++ b/python/vyos/configtree.py
@@ -19,7 +19,9 @@ import logging
from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool
-LIBPATH = '/usr/lib/libvyosconfig.so.0'
+BUILD_PATH = '/tmp/libvyosconfig/_build/libvyosconfig.so'
+INSTALL_PATH = '/usr/lib/libvyosconfig.so.0'
+LIBPATH = BUILD_PATH if os.path.isfile(BUILD_PATH) else INSTALL_PATH
def replace_backslash(s, search, replace):
@@ -48,7 +50,7 @@ def unescape_backslash(string: str) -> str:
def extract_version(s):
"""Extract the version string from the config string"""
t = re.split('(^//)', s, maxsplit=1, flags=re.MULTILINE)
- return (s, ''.join(t[1:]))
+ return (t[0], ''.join(t[1:]))
def check_path(path):
@@ -64,9 +66,14 @@ class ConfigTreeError(Exception):
class ConfigTree(object):
- def __init__(self, config_string=None, address=None, libpath=LIBPATH):
- if config_string is None and address is None:
- raise TypeError("ConfigTree() requires one of 'config_string' or 'address'")
+ def __init__(
+ self, config_string=None, address=None, internal=None, libpath=LIBPATH
+ ):
+ if config_string is None and address is None and internal is None:
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
+
self.__config = None
self.__lib = cdll.LoadLibrary(libpath)
@@ -87,6 +94,13 @@ class ConfigTree(object):
self.__to_commands.argtypes = [c_void_p, c_char_p]
self.__to_commands.restype = c_char_p
+ self.__read_internal = self.__lib.read_internal
+ self.__read_internal.argtypes = [c_char_p]
+ self.__read_internal.restype = c_void_p
+
+ self.__write_internal = self.__lib.write_internal
+ self.__write_internal.argtypes = [c_void_p, c_char_p]
+
self.__to_json = self.__lib.to_json
self.__to_json.argtypes = [c_void_p]
self.__to_json.restype = c_char_p
@@ -166,7 +180,21 @@ class ConfigTree(object):
self.__destroy = self.__lib.destroy
self.__destroy.argtypes = [c_void_p]
- if address is None:
+ self.__equal = self.__lib.equal
+ self.__equal.argtypes = [c_void_p, c_void_p]
+ self.__equal.restype = c_bool
+
+ if address is not None:
+ self.__config = address
+ self.__version = ''
+ elif internal is not None:
+ config = self.__read_internal(internal.encode())
+ if config is None:
+ msg = self.__get_error().decode()
+ raise ValueError('Failed to read internal rep: {0}'.format(msg))
+ else:
+ self.__config = config
+ elif config_string is not None:
config_section, version_section = extract_version(config_string)
config_section = escape_backslash(config_section)
config = self.__from_string(config_section.encode())
@@ -177,8 +205,9 @@ class ConfigTree(object):
self.__config = config
self.__version = version_section
else:
- self.__config = address
- self.__version = ''
+ raise TypeError(
+ "ConfigTree() requires one of 'config_string', 'address', or 'internal'"
+ )
self.__migration = os.environ.get('VYOS_MIGRATION')
if self.__migration:
@@ -188,6 +217,11 @@ class ConfigTree(object):
if self.__config is not None:
self.__destroy(self.__config)
+ def __eq__(self, other):
+ if isinstance(other, ConfigTree):
+ return self.__equal(self._get_config(), other._get_config())
+ return False
+
def __str__(self):
return self.to_string()
@@ -197,6 +231,9 @@ class ConfigTree(object):
def get_version_string(self):
return self.__version
+ def write_cache(self, file_name):
+ self.__write_internal(self._get_config(), file_name)
+
def to_string(self, ordered_values=False, no_version=False):
config_string = self.__to_string(self.__config, ordered_values).decode()
config_string = unescape_backslash(config_string)
diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py
index 4084425b1..d5f443f15 100644
--- a/python/vyos/configverify.py
+++ b/python/vyos/configverify.py
@@ -92,6 +92,9 @@ def verify_mtu_ipv6(config):
tmp = dict_search('ipv6.address.eui64', config)
if tmp != None: raise ConfigError(error_msg)
+ tmp = dict_search('ipv6.address.interface_identifier', config)
+ if tmp != None: raise ConfigError(error_msg)
+
def verify_vrf(config):
"""
Common helper function used by interface implementations to perform
@@ -356,6 +359,7 @@ def verify_vlan_config(config):
verify_vrf(vlan)
verify_mirror_redirect(vlan)
verify_mtu_parent(vlan, config)
+ verify_mtu_ipv6(vlan)
# 802.1ad (Q-in-Q) VLANs
for s_vlan_id in config.get('vif_s', {}):
@@ -367,6 +371,7 @@ def verify_vlan_config(config):
verify_vrf(s_vlan)
verify_mirror_redirect(s_vlan)
verify_mtu_parent(s_vlan, config)
+ verify_mtu_ipv6(s_vlan)
for c_vlan_id in s_vlan.get('vif_c', {}):
c_vlan = s_vlan['vif_c'][c_vlan_id]
@@ -378,6 +383,7 @@ def verify_vlan_config(config):
verify_mirror_redirect(c_vlan)
verify_mtu_parent(c_vlan, config)
verify_mtu_parent(c_vlan, s_vlan)
+ verify_mtu_ipv6(c_vlan)
def verify_diffie_hellman_length(file, min_keysize):
diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py
index 9757a34df..c1e5ddc04 100644
--- a/python/vyos/defaults.py
+++ b/python/vyos/defaults.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2018-2025 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
@@ -37,7 +37,19 @@ directories = {
'dhcp6_client_dir' : '/run/dhcp6c',
'vyos_configdir' : '/opt/vyatta/config',
'completion_dir' : f'{base_dir}/completion',
- 'ca_certificates' : '/usr/local/share/ca-certificates/vyos'
+ 'ca_certificates' : '/usr/local/share/ca-certificates/vyos',
+ 'ppp_nexthop_dir' : '/run/ppp_nexthop',
+ 'proto_path' : '/usr/share/vyos/vyconf'
+}
+
+systemd_services = {
+ 'haproxy' : 'haproxy.service',
+ 'syslog' : 'syslog.service',
+ 'snmpd' : 'snmpd.service',
+}
+
+internal_ports = {
+ 'certbot_haproxy' : 65080, # Certbot running behing haproxy
}
config_status = '/tmp/vyos-config-status'
@@ -63,3 +75,5 @@ rt_symbolic_names = {
rt_global_vrf = rt_symbolic_names['main']
rt_global_table = rt_symbolic_names['main']
+
+vyconfd_conf = '/etc/vyos/vyconfd.conf'
diff --git a/python/vyos/firewall.py b/python/vyos/firewall.py
index 314e8dfe3..64022db84 100755
--- a/python/vyos/firewall.py
+++ b/python/vyos/firewall.py
@@ -233,6 +233,9 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
hook_name = 'prerouting'
if hook == 'NAM':
hook_name = f'name'
+ # for policy
+ if hook == 'route' or hook == 'route6':
+ hook_name = hook
output.append(f'{ip_name} {prefix}addr {operator} @GEOIP_CC{def_suffix}_{hook_name}_{fw_name}_{rule_id}')
if 'mac_address' in side_conf:
@@ -310,6 +313,16 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
operator = '!='
group_name = group_name[1:]
output.append(f'{ip_name} {prefix}addr {operator} @D_{group_name}')
+ elif 'remote_group' in group:
+ group_name = group['remote_group']
+ operator = ''
+ if group_name[0] == '!':
+ operator = '!='
+ group_name = group_name[1:]
+ if ip_name == 'ip':
+ output.append(f'{ip_name} {prefix}addr {operator} @R_{group_name}')
+ elif ip_name == 'ip6':
+ output.append(f'{ip_name} {prefix}addr {operator} @R6_{group_name}')
if 'mac_group' in group:
group_name = group['mac_group']
operator = ''
@@ -461,14 +474,14 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
output.append('gre version 1')
if gre_key:
- # The offset of the key within the packet shifts depending on the C-flag.
- # nftables cannot handle complex enough expressions to match multiple
+ # The offset of the key within the packet shifts depending on the C-flag.
+ # nftables cannot handle complex enough expressions to match multiple
# offsets based on bitfields elsewhere.
- # We enforce a specific match for the checksum flag in validation, so the
- # gre_flags dict will always have a 'checksum' key when gre_key is populated.
- if not gre_flags['checksum']:
+ # We enforce a specific match for the checksum flag in validation, so the
+ # gre_flags dict will always have a 'checksum' key when gre_key is populated.
+ if not gre_flags['checksum']:
# No "unset" child node means C is set, we offset key lookup +32 bits
- output.append(f'@th,64,32 == {gre_key}')
+ output.append(f'@th,64,32 == {gre_key}')
else:
output.append(f'@th,32,32 == {gre_key}')
@@ -627,7 +640,7 @@ def parse_rule(rule_conf, hook, fw_name, rule_id, ip_name):
return " ".join(output)
def parse_gre_flags(flags, force_keyed=False):
- flag_map = { # nft does not have symbolic names for these.
+ flag_map = { # nft does not have symbolic names for these.
'checksum': 1<<0,
'routing': 1<<1,
'key': 1<<2,
@@ -638,7 +651,7 @@ def parse_gre_flags(flags, force_keyed=False):
include = 0
exclude = 0
for fl_name, fl_state in flags.items():
- if not fl_state:
+ if not fl_state:
include |= flag_map[fl_name]
else: # 'unset' child tag
exclude |= flag_map[fl_name]
@@ -731,14 +744,14 @@ class GeoIPLock(object):
def __exit__(self, exc_type, exc_value, tb):
os.unlink(self.file)
-def geoip_update(firewall, force=False):
+def geoip_update(firewall=None, policy=None, force=False):
with GeoIPLock(geoip_lock_file) as lock:
if not lock:
print("Script is already running")
return False
- if not firewall:
- print("Firewall is not configured")
+ if not firewall and not policy:
+ print("Firewall and policy are not configured")
return True
if not os.path.exists(geoip_database):
@@ -753,23 +766,41 @@ def geoip_update(firewall, force=False):
ipv4_sets = {}
ipv6_sets = {}
+ ipv4_codes_policy = {}
+ ipv6_codes_policy = {}
+
+ ipv4_sets_policy = {}
+ ipv6_sets_policy = {}
+
# Map country codes to set names
- for codes, path in dict_search_recursive(firewall, 'country_code'):
- set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
- if ( path[0] == 'ipv4'):
- for code in codes:
- ipv4_codes.setdefault(code, []).append(set_name)
- elif ( path[0] == 'ipv6' ):
- set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
- for code in codes:
- ipv6_codes.setdefault(code, []).append(set_name)
-
- if not ipv4_codes and not ipv6_codes:
+ if firewall:
+ for codes, path in dict_search_recursive(firewall, 'country_code'):
+ set_name = f'GEOIP_CC_{path[1]}_{path[2]}_{path[4]}'
+ if ( path[0] == 'ipv4'):
+ for code in codes:
+ ipv4_codes.setdefault(code, []).append(set_name)
+ elif ( path[0] == 'ipv6' ):
+ set_name = f'GEOIP_CC6_{path[1]}_{path[2]}_{path[4]}'
+ for code in codes:
+ ipv6_codes.setdefault(code, []).append(set_name)
+
+ if policy:
+ for codes, path in dict_search_recursive(policy, 'country_code'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if ( path[0] == 'route'):
+ for code in codes:
+ ipv4_codes_policy.setdefault(code, []).append(set_name)
+ elif ( path[0] == 'route6' ):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ for code in codes:
+ ipv6_codes_policy.setdefault(code, []).append(set_name)
+
+ if not ipv4_codes and not ipv6_codes and not ipv4_codes_policy and not ipv6_codes_policy:
if force:
- print("GeoIP not in use by firewall")
+ print("GeoIP not in use by firewall and policy")
return True
- geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes])
+ geoip_data = geoip_load_data([*ipv4_codes, *ipv6_codes, *ipv4_codes_policy, *ipv6_codes_policy])
# Iterate IP blocks to assign to sets
for start, end, code in geoip_data:
@@ -778,19 +809,29 @@ def geoip_update(firewall, force=False):
ip_range = f'{start}-{end}' if start != end else start
for setname in ipv4_codes[code]:
ipv4_sets.setdefault(setname, []).append(ip_range)
+ if code in ipv4_codes_policy and ipv4:
+ ip_range = f'{start}-{end}' if start != end else start
+ for setname in ipv4_codes_policy[code]:
+ ipv4_sets_policy.setdefault(setname, []).append(ip_range)
if code in ipv6_codes and not ipv4:
ip_range = f'{start}-{end}' if start != end else start
for setname in ipv6_codes[code]:
ipv6_sets.setdefault(setname, []).append(ip_range)
+ if code in ipv6_codes_policy and not ipv4:
+ ip_range = f'{start}-{end}' if start != end else start
+ for setname in ipv6_codes_policy[code]:
+ ipv6_sets_policy.setdefault(setname, []).append(ip_range)
render(nftables_geoip_conf, 'firewall/nftables-geoip-update.j2', {
'ipv4_sets': ipv4_sets,
- 'ipv6_sets': ipv6_sets
+ 'ipv6_sets': ipv6_sets,
+ 'ipv4_sets_policy': ipv4_sets_policy,
+ 'ipv6_sets_policy': ipv6_sets_policy,
})
result = run(f'nft --file {nftables_geoip_conf}')
if result != 0:
- print('Error: GeoIP failed to update firewall')
+ print('Error: GeoIP failed to update firewall/policy')
return False
return True
diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py
index ba44978d1..73d6dd5f0 100644
--- a/python/vyos/frrender.py
+++ b/python/vyos/frrender.py
@@ -60,6 +60,10 @@ def get_frrender_dict(conf, argv=None) -> dict:
from vyos.configdict import get_dhcp_interfaces
from vyos.configdict import get_pppoe_interfaces
+ # We need to re-set the CLI path to the root level, as this function uses
+ # conf.exists() with an absolute path form the CLI root
+ conf.set_level([])
+
# Create an empty dictionary which will be filled down the code path and
# returned to the caller
dict = {}
@@ -88,7 +92,7 @@ def get_frrender_dict(conf, argv=None) -> dict:
if dict_search(f'area.{area_num}.area_type.nssa', ospf) is None:
del default_values['area'][area_num]['area_type']['nssa']
- for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']:
+ for protocol in ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']:
if dict_search(f'redistribute.{protocol}', ospf) is None:
del default_values['redistribute'][protocol]
if not bool(default_values['redistribute']):
@@ -599,8 +603,10 @@ def get_frrender_dict(conf, argv=None) -> dict:
dict.update({'vrf' : vrf})
if os.path.exists(frr_debug_enable):
+ print(f'---- get_frrender_dict({conf}) ----')
import pprint
pprint.pprint(dict)
+ print('-----------------------------------')
return dict
@@ -691,6 +697,9 @@ class FRRender:
debug('FRR: START CONFIGURATION RENDERING')
# we can not reload an empty file, thus we always embed the marker
output = '!\n'
+ # Enable FRR logging
+ output += 'log syslog\n'
+ output += 'log facility local7\n'
# Enable SNMP agentx support
# SNMP AgentX support cannot be disabled once enabled
if 'snmp' in config_dict:
diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py
index d534dade7..f81026965 100644
--- a/python/vyos/ifconfig/bridge.py
+++ b/python/vyos/ifconfig/bridge.py
@@ -19,7 +19,7 @@ from vyos.utils.assertion import assert_list
from vyos.utils.assertion import assert_positive
from vyos.utils.dict import dict_search
from vyos.utils.network import interface_exists
-from vyos.configdict import get_vlan_ids
+from vyos.configdict import get_vlans_ids_and_range
from vyos.configdict import list_diff
@Interface.register
@@ -380,7 +380,7 @@ class BridgeIf(Interface):
add_vlan = []
native_vlan_id = None
allowed_vlan_ids= []
- cur_vlan_ids = get_vlan_ids(interface)
+ cur_vlan_ids = get_vlans_ids_and_range(interface)
if 'native_vlan' in interface_config:
vlan_id = interface_config['native_vlan']
@@ -389,14 +389,8 @@ class BridgeIf(Interface):
if 'allowed_vlan' in interface_config:
for vlan in interface_config['allowed_vlan']:
- vlan_range = vlan.split('-')
- if len(vlan_range) == 2:
- for vlan_add in range(int(vlan_range[0]),int(vlan_range[1]) + 1):
- add_vlan.append(str(vlan_add))
- allowed_vlan_ids.append(str(vlan_add))
- else:
- add_vlan.append(vlan)
- allowed_vlan_ids.append(vlan)
+ add_vlan.append(vlan)
+ allowed_vlan_ids.append(vlan)
# Remove redundant VLANs from the system
for vlan in list_diff(cur_vlan_ids, add_vlan):
diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py
index f7fddb812..f53ef4166 100644
--- a/python/vyos/ifconfig/geneve.py
+++ b/python/vyos/ifconfig/geneve.py
@@ -48,7 +48,7 @@ class GeneveIf(Interface):
'parameters.ipv6.flowlabel' : 'flowlabel',
}
- cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}'
+ cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote} dstport {port}'
for vyos_key, iproute2_key in mapping.items():
# dict_search will return an empty dict "{}" for valueless nodes like
# "parameters.nolearning" - thus we need to test the nodes existence
diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py
index cb73e2597..003a273c0 100644
--- a/python/vyos/ifconfig/interface.py
+++ b/python/vyos/ifconfig/interface.py
@@ -22,6 +22,7 @@ from copy import deepcopy
from glob import glob
from ipaddress import IPv4Network
+from ipaddress import IPv6Interface
from netifaces import ifaddresses
# this is not the same as socket.AF_INET/INET6
from netifaces import AF_INET
@@ -595,12 +596,16 @@ class Interface(Control):
"""
Add/Remove interface from given VRF instance.
+ Keyword arguments:
+ vrf: VRF instance name or empty string (default VRF)
+
+ Return True if VRF was changed, False otherwise
+
Example:
>>> from vyos.ifconfig import Interface
>>> Interface('eth0').set_vrf('foo')
>>> Interface('eth0').set_vrf()
"""
-
# Don't allow for netns yet
if 'netns' in self.config:
return False
@@ -611,21 +616,33 @@ class Interface(Control):
# Get current VRF table ID
old_vrf_tableid = get_vrf_tableid(self.ifname)
- self.set_interface('vrf', vrf)
+ # Always stop the DHCP client process to clean up routes within the VRF
+ # where the process was originally started. There is no need to add a
+ # condition to only call the method if "address dhcp" was defined, as
+ # this is handled inside set_dhcp(v6) by only stopping if the daemon is
+ # running. DHCP client process restart will be handled later on once the
+ # interface is moved to the new VRF.
+ self.set_dhcp(False)
+ self.set_dhcpv6(False)
+
+ # Move interface in/out of VRF
+ self.set_interface('vrf', vrf)
if vrf:
# Get routing table ID number for VRF
vrf_table_id = get_vrf_tableid(vrf)
# Add map element with interface and zone ID
- if vrf_table_id:
+ if vrf_table_id and old_vrf_tableid != vrf_table_id:
# delete old table ID from nftables if it has changed, e.g. interface moved to a different VRF
- if old_vrf_tableid and old_vrf_tableid != int(vrf_table_id):
- self._del_interface_from_ct_iface_map()
+ self._del_interface_from_ct_iface_map()
self._add_interface_to_ct_iface_map(vrf_table_id)
+ return True
else:
- self._del_interface_from_ct_iface_map()
+ if old_vrf_tableid != get_vrf_tableid(self.ifname):
+ self._del_interface_from_ct_iface_map()
+ return True
- return True
+ return False
def set_arp_cache_tmo(self, tmo):
"""
@@ -893,7 +910,11 @@ class Interface(Control):
tmp = self.get_interface('ipv6_autoconf')
if tmp == autoconf:
return None
- return self.set_interface('ipv6_autoconf', autoconf)
+ rc = self.set_interface('ipv6_autoconf', autoconf)
+ if autoconf == '0':
+ flushed = self.flush_ipv6_slaac_addrs()
+ self.flush_ipv6_slaac_routes(ra_addrs=flushed)
+ return rc
def add_ipv6_eui64_address(self, prefix):
"""
@@ -921,6 +942,20 @@ class Interface(Control):
prefixlen = prefix.split('/')[1]
self.del_addr(f'{eui64}/{prefixlen}')
+ def set_ipv6_interface_identifier(self, identifier):
+ """
+ Set the interface identifier for IPv6 autoconf.
+ """
+ cmd = f'ip token set {identifier} dev {self.ifname}'
+ self._cmd(cmd)
+
+ def del_ipv6_interface_identifier(self):
+ """
+ Delete the interface identifier for IPv6 autoconf.
+ """
+ cmd = f'ip token delete dev {self.ifname}'
+ self._cmd(cmd)
+
def set_ipv6_forwarding(self, forwarding):
"""
Configure IPv6 interface-specific Host/Router behaviour.
@@ -1181,7 +1216,7 @@ class Interface(Control):
"""
return self.get_addr_v4() + self.get_addr_v6()
- def add_addr(self, addr):
+ def add_addr(self, addr: str, vrf_changed: bool=False) -> bool:
"""
Add IP(v6) address to interface. Address is only added if it is not
already assigned to that interface. Address format must be validated
@@ -1214,15 +1249,14 @@ class Interface(Control):
# add to interface
if addr == 'dhcp':
- self.set_dhcp(True)
+ self.set_dhcp(True, vrf_changed=vrf_changed)
elif addr == 'dhcpv6':
- self.set_dhcpv6(True)
+ self.set_dhcpv6(True, vrf_changed=vrf_changed)
elif not is_intf_addr_assigned(self.ifname, addr, netns=netns):
netns_cmd = f'ip netns exec {netns}' if netns else ''
tmp = f'{netns_cmd} ip addr add {addr} dev {self.ifname}'
# Add broadcast address for IPv4
if is_ipv4(addr): tmp += ' brd +'
-
self._cmd(tmp)
else:
return False
@@ -1232,7 +1266,7 @@ class Interface(Control):
return True
- def del_addr(self, addr):
+ def del_addr(self, addr: str) -> bool:
"""
Delete IP(v6) address from interface. Address is only deleted if it is
assigned to that interface. Address format must be exactly the same as
@@ -1295,6 +1329,71 @@ class Interface(Control):
# flush all addresses
self._cmd(cmd)
+ def flush_ipv6_slaac_addrs(self) -> list:
+ """
+ Flush all IPv6 addresses installed in response to router advertisement
+ messages from this interface.
+
+ Will raise an exception on error.
+ Will return a list of flushed IPv6 addresses.
+ """
+ netns = get_interface_namespace(self.ifname)
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+ tmp = get_interface_address(self.ifname)
+ if not tmp or 'addr_info' not in tmp:
+ return
+
+ # Parse interface IP addresses. Example data:
+ # {'family': 'inet6', 'local': '2001:db8:1111:0:250:56ff:feb3:38c5',
+ # 'prefixlen': 64, 'scope': 'global', 'dynamic': True,
+ # 'mngtmpaddr': True, 'protocol': 'kernel_ra',
+ # 'valid_life_time': 2591987, 'preferred_life_time': 14387}
+ flushed = []
+ for addr_info in tmp['addr_info']:
+ if 'protocol' not in addr_info:
+ continue
+ if (addr_info['protocol'] == 'kernel_ra' and
+ addr_info['scope'] == 'global'):
+ # Flush IPv6 addresses installed by router advertisement
+ ra_addr = f"{addr_info['local']}/{addr_info['prefixlen']}"
+ flushed.append(ra_addr)
+ cmd = f'{netns_cmd} ip -6 addr del dev {self.ifname} {ra_addr}'
+ self._cmd(cmd)
+ return flushed
+
+ def flush_ipv6_slaac_routes(self, ra_addrs: list=[]) -> None:
+ """
+ Flush IPv6 default routes installed in response to router advertisement
+ messages from this interface.
+
+ Will raise an exception on error.
+ """
+ # Find IPv6 connected prefixes for flushed SLAAC addresses
+ connected = []
+ for addr in ra_addrs if isinstance(ra_addrs, list) else []:
+ connected.append(str(IPv6Interface(addr).network))
+
+ netns = get_interface_namespace(self.ifname)
+ netns_cmd = f'ip netns exec {netns}' if netns else ''
+
+ tmp = self._cmd(f'{netns_cmd} ip -j -6 route show dev {self.ifname}')
+ tmp = json.loads(tmp)
+ # Parse interface routes. Example data:
+ # {'dst': 'default', 'gateway': 'fe80::250:56ff:feb3:cdba',
+ # 'protocol': 'ra', 'metric': 1024, 'flags': [], 'expires': 1398,
+ # 'metrics': [{'hoplimit': 64}], 'pref': 'medium'}
+ for route in tmp:
+ # If it's a default route received from RA, delete it
+ if (dict_search('dst', route) == 'default' and
+ dict_search('protocol', route) == 'ra'):
+ self._cmd(f'{netns_cmd} ip -6 route del default via {route["gateway"]} dev {self.ifname}')
+ # Remove connected prefixes received from RA
+ if dict_search('dst', route) in connected:
+ # If it's a connected prefix, delete it
+ self._cmd(f'{netns_cmd} ip -6 route del {route["dst"]} dev {self.ifname}')
+
+ return None
+
def add_to_bridge(self, bridge_dict):
"""
Adds the interface to the bridge with the passed port config.
@@ -1305,8 +1404,6 @@ class Interface(Control):
# drop all interface addresses first
self.flush_addrs()
- ifname = self.ifname
-
for bridge, bridge_config in bridge_dict.items():
# add interface to bridge - use Section.klass to get BridgeIf class
Section.klass(bridge)(bridge, create=True).add_port(self.ifname)
@@ -1322,7 +1419,7 @@ class Interface(Control):
bridge_vlan_filter = Section.klass(bridge)(bridge, create=True).get_vlan_filter()
if int(bridge_vlan_filter):
- cur_vlan_ids = get_vlan_ids(ifname)
+ cur_vlan_ids = get_vlan_ids(self.ifname)
add_vlan = []
native_vlan_id = None
allowed_vlan_ids= []
@@ -1345,18 +1442,18 @@ class Interface(Control):
# Remove redundant VLANs from the system
for vlan in list_diff(cur_vlan_ids, add_vlan):
- cmd = f'bridge vlan del dev {ifname} vid {vlan} master'
+ cmd = f'bridge vlan del dev {self.ifname} vid {vlan} master'
self._cmd(cmd)
for vlan in allowed_vlan_ids:
- cmd = f'bridge vlan add dev {ifname} vid {vlan} master'
+ cmd = f'bridge vlan add dev {self.ifname} vid {vlan} master'
self._cmd(cmd)
# Setting native VLAN to system
if native_vlan_id:
- cmd = f'bridge vlan add dev {ifname} vid {native_vlan_id} pvid untagged master'
+ cmd = f'bridge vlan add dev {self.ifname} vid {native_vlan_id} pvid untagged master'
self._cmd(cmd)
- def set_dhcp(self, enable):
+ def set_dhcp(self, enable: bool, vrf_changed: bool=False):
"""
Enable/Disable DHCP client on a given interface.
"""
@@ -1396,7 +1493,9 @@ class Interface(Control):
# the old lease is released a new one is acquired (T4203). We will
# only restart DHCP client if it's option changed, or if it's not
# running, but it should be running (e.g. on system startup)
- if 'dhcp_options_changed' in self.config or not is_systemd_service_active(systemd_service):
+ if (vrf_changed or
+ ('dhcp_options_changed' in self.config) or
+ (not is_systemd_service_active(systemd_service))):
return self._cmd(f'systemctl restart {systemd_service}')
else:
if is_systemd_service_active(systemd_service):
@@ -1423,19 +1522,18 @@ class Interface(Control):
return None
- def set_dhcpv6(self, enable):
+ def set_dhcpv6(self, enable: bool, vrf_changed: bool=False):
"""
Enable/Disable DHCPv6 client on a given interface.
"""
if enable not in [True, False]:
raise ValueError()
- ifname = self.ifname
config_base = directories['dhcp6_client_dir']
- config_file = f'{config_base}/dhcp6c.{ifname}.conf'
- script_file = f'/etc/wide-dhcpv6/dhcp6c.{ifname}.script' # can not live under /run b/c of noexec mount option
- systemd_override_file = f'/run/systemd/system/dhcp6c@{ifname}.service.d/10-override.conf'
- systemd_service = f'dhcp6c@{ifname}.service'
+ config_file = f'{config_base}/dhcp6c.{self.ifname}.conf'
+ script_file = f'/etc/wide-dhcpv6/dhcp6c.{self.ifname}.script' # can not live under /run b/c of noexec mount option
+ systemd_override_file = f'/run/systemd/system/dhcp6c@{self.ifname}.service.d/10-override.conf'
+ systemd_service = f'dhcp6c@{self.ifname}.service'
# Rendered client configuration files require additional settings
config = deepcopy(self.config)
@@ -1452,7 +1550,10 @@ class Interface(Control):
# We must ignore any return codes. This is required to enable
# DHCPv6-PD for interfaces which are yet not up and running.
- return self._popen(f'systemctl restart {systemd_service}')
+ if (vrf_changed or
+ ('dhcpv6_options_changed' in self.config) or
+ (not is_systemd_service_active(systemd_service))):
+ return self._popen(f'systemctl restart {systemd_service}')
else:
if is_systemd_service_active(systemd_service):
self._cmd(f'systemctl stop {systemd_service}')
@@ -1669,30 +1770,31 @@ class Interface(Control):
else:
self.del_addr(addr)
- # start DHCPv6 client when only PD was configured
- if dhcpv6pd:
- self.set_dhcpv6(True)
-
# XXX: Bind interface to given VRF or unbind it if vrf is not set. Unbinding
# will call 'ip link set dev eth0 nomaster' which will also drop the
# interface out of any bridge or bond - thus this is checked before.
+ vrf_changed = False
if 'is_bond_member' in config:
bond_if = next(iter(config['is_bond_member']))
tmp = get_interface_config(config['ifname'])
if 'master' in tmp and tmp['master'] != bond_if:
- self.set_vrf('')
+ vrf_changed = self.set_vrf('')
elif 'is_bridge_member' in config:
bridge_if = next(iter(config['is_bridge_member']))
tmp = get_interface_config(config['ifname'])
if 'master' in tmp and tmp['master'] != bridge_if:
- self.set_vrf('')
+ vrf_changed = self.set_vrf('')
else:
- self.set_vrf(config.get('vrf', ''))
+ vrf_changed = self.set_vrf(config.get('vrf', ''))
+
+ # start DHCPv6 client when only PD was configured
+ if dhcpv6pd:
+ self.set_dhcpv6(True, vrf_changed=vrf_changed)
# Add this section after vrf T4331
for addr in new_addr:
- self.add_addr(addr)
+ self.add_addr(addr, vrf_changed=vrf_changed)
# Configure MSS value for IPv4 TCP connections
tmp = dict_search('ip.adjust_mss', config)
@@ -1771,11 +1873,26 @@ class Interface(Control):
value = '0' if (tmp != None) else '1'
self.set_ipv6_forwarding(value)
+ # Delete old interface identifier
+ # This should be before setting the accept_ra value
+ old = dict_search('ipv6.address.interface_identifier_old', config)
+ now = dict_search('ipv6.address.interface_identifier', config)
+ if old and not now:
+ # accept_ra of ra is required to delete the interface identifier
+ self.set_ipv6_accept_ra('2')
+ self.del_ipv6_interface_identifier()
+
+ # Set IPv6 Interface identifier
+ # This should be before setting the accept_ra value
+ tmp = dict_search('ipv6.address.interface_identifier', config)
+ if tmp:
+ # accept_ra is required to set the interface identifier
+ self.set_ipv6_accept_ra('2')
+ self.set_ipv6_interface_identifier(tmp)
+
# IPv6 router advertisements
tmp = dict_search('ipv6.address.autoconf', config)
- value = '2' if (tmp != None) else '1'
- if 'dhcpv6' in new_addr:
- value = '2'
+ value = '2' if (tmp != None) else '0'
self.set_ipv6_accept_ra(value)
# IPv6 address autoconfiguration
diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py
index 341fd32ff..f5217aecb 100644
--- a/python/vyos/ifconfig/wireguard.py
+++ b/python/vyos/ifconfig/wireguard.py
@@ -82,6 +82,84 @@ class WireGuardOperational(Operational):
}
return output
+ def show_interface(self):
+ from vyos.config import Config
+
+ c = Config()
+
+ wgdump = self._dump().get(self.config['ifname'], None)
+
+ c.set_level(['interfaces', 'wireguard', self.config['ifname']])
+ description = c.return_effective_value(['description'])
+ ips = c.return_effective_values(['address'])
+ hostnames = c.return_effective_values(['host-name'])
+
+ answer = 'interface: {}\n'.format(self.config['ifname'])
+ if description:
+ answer += ' description: {}\n'.format(description)
+ if ips:
+ answer += ' address: {}\n'.format(', '.join(ips))
+ if hostnames:
+ answer += ' hostname: {}\n'.format(', '.join(hostnames))
+
+ answer += ' public key: {}\n'.format(wgdump['public_key'])
+ answer += ' private key: (hidden)\n'
+ answer += ' listening port: {}\n'.format(wgdump['listen_port'])
+ answer += '\n'
+
+ for peer in c.list_effective_nodes(['peer']):
+ if wgdump['peers']:
+ pubkey = c.return_effective_value(['peer', peer, 'public-key'])
+ if pubkey in wgdump['peers']:
+ wgpeer = wgdump['peers'][pubkey]
+
+ answer += ' peer: {}\n'.format(peer)
+ answer += ' public key: {}\n'.format(pubkey)
+
+ """ figure out if the tunnel is recently active or not """
+ status = 'inactive'
+ if wgpeer['latest_handshake'] is None:
+ """ no handshake ever """
+ status = 'inactive'
+ else:
+ if int(wgpeer['latest_handshake']) > 0:
+ delta = timedelta(
+ seconds=int(time.time() - wgpeer['latest_handshake'])
+ )
+ answer += ' latest handshake: {}\n'.format(delta)
+ if time.time() - int(wgpeer['latest_handshake']) < (60 * 5):
+ """ Five minutes and the tunnel is still active """
+ status = 'active'
+ else:
+ """ it's been longer than 5 minutes """
+ status = 'inactive'
+ elif int(wgpeer['latest_handshake']) == 0:
+ """ no handshake ever """
+ status = 'inactive'
+ answer += ' status: {}\n'.format(status)
+
+ if wgpeer['endpoint'] is not None:
+ answer += ' endpoint: {}\n'.format(wgpeer['endpoint'])
+
+ if wgpeer['allowed_ips'] is not None:
+ answer += ' allowed ips: {}\n'.format(
+ ','.join(wgpeer['allowed_ips']).replace(',', ', ')
+ )
+
+ if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0:
+ rx_size = size(wgpeer['transfer_rx'], system=alternative)
+ tx_size = size(wgpeer['transfer_tx'], system=alternative)
+ answer += ' transfer: {} received, {} sent\n'.format(
+ rx_size, tx_size
+ )
+
+ if wgpeer['persistent_keepalive'] is not None:
+ answer += ' persistent keepalive: every {} seconds\n'.format(
+ wgpeer['persistent_keepalive']
+ )
+ answer += '\n'
+ return answer
+
def get_latest_handshakes(self):
"""Get latest handshake time for each peer"""
output = {}
diff --git a/python/vyos/kea.py b/python/vyos/kea.py
index 65e2d99b4..5eecbbaad 100644
--- a/python/vyos/kea.py
+++ b/python/vyos/kea.py
@@ -20,8 +20,8 @@ import socket
from datetime import datetime
from datetime import timezone
+from vyos import ConfigError
from vyos.template import is_ipv6
-from vyos.template import isc_static_route
from vyos.template import netmask_from_cidr
from vyos.utils.dict import dict_search_args
from vyos.utils.file import file_permissions
@@ -44,6 +44,7 @@ kea4_options = {
'wpad_url': 'wpad-url',
'ipv6_only_preferred': 'v6-only-preferred',
'captive_portal': 'v4-captive-portal',
+ 'capwap_controller': 'capwap-ac-v4',
}
kea6_options = {
@@ -56,6 +57,7 @@ kea6_options = {
'nisplus_server': 'nisp-servers',
'sntp_server': 'sntp-servers',
'captive_portal': 'v6-captive-portal',
+ 'capwap_controller': 'capwap-ac-v6',
}
kea_ctrl_socket = '/run/kea/dhcp{inet}-ctrl-socket'
@@ -111,22 +113,21 @@ def kea_parse_options(config):
default_route = ''
if 'default_router' in config:
- default_route = isc_static_route('0.0.0.0/0', config['default_router'])
+ default_route = f'0.0.0.0/0 - {config["default_router"]}'
routes = [
- isc_static_route(route, route_options['next_hop'])
+ f'{route} - {route_options["next_hop"]}'
for route, route_options in config['static_route'].items()
]
options.append(
{
- 'name': 'rfc3442-static-route',
+ 'name': 'classless-static-route',
'data': ', '.join(
routes if not default_route else routes + [default_route]
),
}
)
- options.append({'name': 'windows-static-route', 'data': ', '.join(routes)})
if 'time_zone' in config:
with open('/usr/share/zoneinfo/' + config['time_zone'], 'rb') as f:
@@ -147,7 +148,7 @@ def kea_parse_options(config):
def kea_parse_subnet(subnet, config):
- out = {'subnet': subnet, 'id': int(config['subnet_id'])}
+ out = {'subnet': subnet, 'id': int(config['subnet_id']), 'user-context': {}}
if 'option' in config:
out['option-data'] = kea_parse_options(config['option'])
@@ -165,6 +166,9 @@ def kea_parse_subnet(subnet, config):
out['valid-lifetime'] = int(config['lease'])
out['max-valid-lifetime'] = int(config['lease'])
+ if 'ping_check' in config:
+ out['user-context']['enable-ping-check'] = True
+
if 'range' in config:
pools = []
for num, range_config in config['range'].items():
@@ -218,6 +222,9 @@ def kea_parse_subnet(subnet, config):
reservations.append(reservation)
out['reservations'] = reservations
+ if 'dynamic_dns_update' in config:
+ out.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
return out
@@ -347,6 +354,54 @@ def kea6_parse_subnet(subnet, config):
return out
+def kea_parse_tsig_algo(algo_spec):
+ translate = {
+ 'md5': 'HMAC-MD5',
+ 'sha1': 'HMAC-SHA1',
+ 'sha224': 'HMAC-SHA224',
+ 'sha256': 'HMAC-SHA256',
+ 'sha384': 'HMAC-SHA384',
+ 'sha512': 'HMAC-SHA512'
+ }
+ if algo_spec not in translate:
+ raise ConfigError(f'Unsupported TSIG algorithm: {algo_spec}')
+ return translate[algo_spec]
+
+def kea_parse_enable_disable(value):
+ return True if value == 'enable' else False
+
+def kea_parse_ddns_settings(config):
+ data = {}
+
+ if send_updates := config.get('send_updates'):
+ data['ddns-send-updates'] = kea_parse_enable_disable(send_updates)
+
+ if override_client_update := config.get('override_client_update'):
+ data['ddns-override-client-update'] = kea_parse_enable_disable(override_client_update)
+
+ if override_no_update := config.get('override_no_update'):
+ data['ddns-override-no-update'] = kea_parse_enable_disable(override_no_update)
+
+ if update_on_renew := config.get('update_on_renew'):
+ data['ddns-update-on-renew'] = kea_parse_enable_disable(update_on_renew)
+
+ if conflict_resolution := config.get('conflict_resolution'):
+ data['ddns-use-conflict-resolution'] = kea_parse_enable_disable(conflict_resolution)
+
+ if 'replace_client_name' in config:
+ data['ddns-replace-client-name'] = config['replace_client_name']
+ if 'generated_prefix' in config:
+ data['ddns-generated-prefix'] = config['generated_prefix']
+ if 'qualifying_suffix' in config:
+ data['ddns-qualifying-suffix'] = config['qualifying_suffix']
+ if 'ttl_percent' in config:
+ data['ddns-ttl-percent'] = int(config['ttl_percent']) / 100
+ if 'hostname_char_set' in config:
+ data['hostname-char-set'] = config['hostname_char_set']
+ if 'hostname_char_replacement' in config:
+ data['hostname-char-replacement'] = config['hostname_char_replacement']
+
+ return data
def _ctrl_socket_command(inet, command, args=None):
path = kea_ctrl_socket.format(inet=inet)
@@ -384,6 +439,41 @@ def kea_get_leases(inet):
return leases['arguments']['leases']
+def kea_add_lease(
+ inet,
+ ip_address,
+ host_name=None,
+ mac_address=None,
+ iaid=None,
+ duid=None,
+ subnet_id=None,
+):
+ args = {'ip-address': ip_address}
+
+ if host_name:
+ args['hostname'] = host_name
+
+ if subnet_id:
+ args['subnet-id'] = subnet_id
+
+ # IPv4 requires MAC address, IPv6 requires either MAC address or DUID
+ if mac_address:
+ args['hw-address'] = mac_address
+ if duid:
+ args['duid'] = duid
+
+ # IPv6 requires IAID
+ if inet == '6' and iaid:
+ args['iaid'] = iaid
+
+ result = _ctrl_socket_command(inet, f'lease{inet}-add', args)
+
+ if result and 'result' in result:
+ return result['result'] == 0
+
+ return False
+
+
def kea_delete_lease(inet, ip_address):
args = {'ip-address': ip_address}
@@ -430,6 +520,32 @@ def kea_get_pool_from_subnet_id(config, inet, subnet_id):
return None
+def kea_get_domain_from_subnet_id(config, inet, subnet_id):
+ shared_networks = dict_search_args(
+ config, 'arguments', f'Dhcp{inet}', 'shared-networks'
+ )
+
+ if not shared_networks:
+ return None
+
+ for network in shared_networks:
+ if f'subnet{inet}' not in network:
+ continue
+
+ for subnet in network[f'subnet{inet}']:
+ if 'id' in subnet and int(subnet['id']) == int(subnet_id):
+ for option in subnet['option-data']:
+ if option['name'] == 'domain-name':
+ return option['data']
+
+ # domain-name is not found in subnet, fallback to shared-network pool option
+ for option in network['option-data']:
+ if option['name'] == 'domain-name':
+ return option['data']
+
+ return None
+
+
def kea_get_static_mappings(config, inet, pools=[]) -> list:
"""
Get DHCP static mapping from active Kea DHCPv4 or DHCPv6 configuration
@@ -491,6 +607,11 @@ def kea_get_server_leases(config, inet, pools=[], state=[], origin=None) -> list
if config
else '-'
)
+ data_lease['domain'] = (
+ kea_get_domain_from_subnet_id(config, inet, lease['subnet-id'])
+ if config
+ else ''
+ )
data_lease['end'] = (
lease['expire_time'].timestamp() if lease['expire_time'] else None
)
diff --git a/python/vyos/proto/__init__.py b/python/vyos/proto/__init__.py
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/python/vyos/proto/__init__.py
diff --git a/python/vyos/proto/generate_dataclass.py b/python/vyos/proto/generate_dataclass.py
new file mode 100755
index 000000000..c6296c568
--- /dev/null
+++ b/python/vyos/proto/generate_dataclass.py
@@ -0,0 +1,178 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 argparse
+import os
+
+from google.protobuf.descriptor_pb2 import FileDescriptorSet # pylint: disable=no-name-in-module
+from google.protobuf.descriptor_pb2 import FieldDescriptorProto # pylint: disable=no-name-in-module
+from humps import decamelize
+
+HEADER = """\
+from enum import IntEnum
+from dataclasses import dataclass
+from dataclasses import field
+"""
+
+
+def normalize(s: str) -> str:
+ """Decamelize and avoid syntactic collision"""
+ t = decamelize(s)
+ return t + '_' if t in ['from'] else t
+
+
+def generate_dataclass(descriptor_proto):
+ class_name = descriptor_proto.name
+ fields = []
+ for field_p in descriptor_proto.field:
+ field_name = field_p.name
+ field_type, field_default = get_type(field_p.type, field_p.type_name)
+ match field_p.label:
+ case FieldDescriptorProto.LABEL_REPEATED:
+ field_type = f'list[{field_type}] = field(default_factory=list)'
+ case FieldDescriptorProto.LABEL_OPTIONAL:
+ field_type = f'{field_type} = None'
+ case _:
+ field_type = f'{field_type} = {field_default}'
+
+ fields.append(f' {field_name}: {field_type}')
+
+ code = f"""
+@dataclass
+class {class_name}:
+{chr(10).join(fields) if fields else ' pass'}
+"""
+
+ return code
+
+
+def generate_request(descriptor_proto):
+ class_name = descriptor_proto.name
+ fields = []
+ f_vars = []
+ for field_p in descriptor_proto.field:
+ field_name = field_p.name
+ field_type, field_default = get_type(field_p.type, field_p.type_name)
+ match field_p.label:
+ case FieldDescriptorProto.LABEL_REPEATED:
+ field_type = f'list[{field_type}] = []'
+ case FieldDescriptorProto.LABEL_OPTIONAL:
+ field_type = f'{field_type} = None'
+ case _:
+ field_type = f'{field_type} = {field_default}'
+
+ fields.append(f'{normalize(field_name)}: {field_type}')
+ f_vars.append(f'{normalize(field_name)}')
+
+ fields.insert(0, 'token: str = None')
+
+ code = f"""
+def set_request_{decamelize(class_name)}({', '.join(fields)}):
+ reqi = {class_name} ({', '.join(f_vars)})
+ req = Request({decamelize(class_name)}=reqi)
+ req_env = RequestEnvelope(token, req)
+ return req_env
+"""
+
+ return code
+
+
+def generate_nested_dataclass(descriptor_proto):
+ out = ''
+ for nested_p in descriptor_proto.nested_type:
+ out = out + generate_dataclass(nested_p)
+
+ return out
+
+
+def generate_nested_request(descriptor_proto):
+ out = ''
+ for nested_p in descriptor_proto.nested_type:
+ out = out + generate_request(nested_p)
+
+ return out
+
+
+def generate_enum_dataclass(descriptor_proto):
+ code = ''
+ for enum_p in descriptor_proto.enum_type:
+ enums = []
+ enum_name = enum_p.name
+ for enum_val in enum_p.value:
+ enums.append(f' {enum_val.name} = {enum_val.number}')
+
+ code += f"""
+class {enum_name}(IntEnum):
+{chr(10).join(enums)}
+"""
+
+ return code
+
+
+def get_type(field_type, type_name):
+ res = 'Any', None
+ match field_type:
+ case FieldDescriptorProto.TYPE_STRING:
+ res = 'str', '""'
+ case FieldDescriptorProto.TYPE_INT32 | FieldDescriptorProto.TYPE_INT64:
+ res = 'int', 0
+ case FieldDescriptorProto.TYPE_FLOAT | FieldDescriptorProto.TYPE_DOUBLE:
+ res = 'float', 0.0
+ case FieldDescriptorProto.TYPE_BOOL:
+ res = 'bool', False
+ case FieldDescriptorProto.TYPE_MESSAGE | FieldDescriptorProto.TYPE_ENUM:
+ res = type_name.split('.')[-1], None
+ case _:
+ pass
+
+ return res
+
+
+if __name__ == '__main__':
+ parser = argparse.ArgumentParser()
+ parser.add_argument('descriptor_file', help='protobuf .desc file')
+ parser.add_argument('--out-dir', help='directory to write generated file')
+ args = parser.parse_args()
+ desc_file = args.descriptor_file
+ out_dir = args.out_dir
+
+ with open(desc_file, 'rb') as f:
+ descriptor_set_data = f.read()
+
+ descriptor_set = FileDescriptorSet()
+ descriptor_set.ParseFromString(descriptor_set_data)
+
+ for file_proto in descriptor_set.file:
+ f = f'{file_proto.name.replace(".", "_")}.py'
+ f = os.path.join(out_dir, f)
+ dataclass_code = ''
+ nested_code = ''
+ enum_code = ''
+ request_code = ''
+ with open(f, 'w') as f:
+ enum_code += generate_enum_dataclass(file_proto)
+ for message_proto in file_proto.message_type:
+ dataclass_code += generate_dataclass(message_proto)
+ nested_code += generate_nested_dataclass(message_proto)
+ enum_code += generate_enum_dataclass(message_proto)
+ request_code += generate_nested_request(message_proto)
+
+ f.write(HEADER)
+ f.write(enum_code)
+ f.write(nested_code)
+ f.write(dataclass_code)
+ f.write(request_code)
diff --git a/python/vyos/proto/vyconf_client.py b/python/vyos/proto/vyconf_client.py
new file mode 100644
index 000000000..b385f0951
--- /dev/null
+++ b/python/vyos/proto/vyconf_client.py
@@ -0,0 +1,89 @@
+# Copyright 2025 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/>.
+
+import socket
+from dataclasses import asdict
+
+from vyos.proto import vyconf_proto
+from vyos.proto import vyconf_pb2
+
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.json_format import ParseDict
+
+socket_path = '/var/run/vyconfd.sock'
+
+
+def send_socket(msg: bytearray) -> bytes:
+ data = bytes()
+ client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+ client.connect(socket_path)
+ client.sendall(msg)
+
+ data_length = client.recv(4)
+ if data_length:
+ length = int.from_bytes(data_length)
+ data = client.recv(length)
+
+ client.close()
+
+ return data
+
+
+def request_to_msg(req: vyconf_proto.RequestEnvelope) -> vyconf_pb2.RequestEnvelope:
+ # pylint: disable=no-member
+
+ msg = vyconf_pb2.RequestEnvelope()
+ msg = ParseDict(asdict(req), msg, ignore_unknown_fields=True)
+ return msg
+
+
+def msg_to_response(msg: vyconf_pb2.Response) -> vyconf_proto.Response:
+ # pylint: disable=no-member
+
+ d = MessageToDict(
+ msg, preserving_proto_field_name=True, use_integers_for_enums=True
+ )
+
+ response = vyconf_proto.Response(**d)
+ return response
+
+
+def write_request(req: vyconf_proto.RequestEnvelope) -> bytearray:
+ req_msg = request_to_msg(req)
+ encoded_data = req_msg.SerializeToString()
+ byte_size = req_msg.ByteSize()
+ length_bytes = byte_size.to_bytes(4)
+ arr = bytearray(length_bytes)
+ arr.extend(encoded_data)
+
+ return arr
+
+
+def read_response(msg: bytes) -> vyconf_proto.Response:
+ response_msg = vyconf_pb2.Response() # pylint: disable=no-member
+ response_msg.ParseFromString(msg)
+ response = msg_to_response(response_msg)
+
+ return response
+
+
+def send_request(name, *args, **kwargs):
+ func = getattr(vyconf_proto, f'set_request_{name}')
+ request_env = func(*args, **kwargs)
+ msg = write_request(request_env)
+ response_msg = send_socket(msg)
+ response = read_response(response_msg)
+
+ return response
diff --git a/python/vyos/system/grub_util.py b/python/vyos/system/grub_util.py
index 4a3d8795e..ad95bb4f9 100644
--- a/python/vyos/system/grub_util.py
+++ b/python/vyos/system/grub_util.py
@@ -56,13 +56,12 @@ def set_kernel_cmdline_options(cmdline_options: str, version: str = '',
@image.if_not_live_boot
def update_kernel_cmdline_options(cmdline_options: str,
- root_dir: str = '') -> None:
+ root_dir: str = '',
+ version = image.get_running_image()) -> 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}'
diff --git a/python/vyos/template.py b/python/vyos/template.py
index be9f781a6..11e1cc50f 100755
--- a/python/vyos/template.py
+++ b/python/vyos/template.py
@@ -36,6 +36,7 @@ DEFAULT_TEMPLATE_DIR = directories["templates"]
# Holds template filters registered via register_filter()
_FILTERS = {}
_TESTS = {}
+_CLEVER_FUNCTIONS = {}
# reuse Environments with identical settings to improve performance
@functools.lru_cache(maxsize=2)
@@ -58,6 +59,7 @@ def _get_environment(location=None):
)
env.filters.update(_FILTERS)
env.tests.update(_TESTS)
+ env.globals.update(_CLEVER_FUNCTIONS)
return env
@@ -77,7 +79,7 @@ def register_filter(name, func=None):
"Filters can only be registered before rendering the first template"
)
if name in _FILTERS:
- raise ValueError(f"A filter with name {name!r} was registered already")
+ raise ValueError(f"A filter with name {name!r} was already registered")
_FILTERS[name] = func
return func
@@ -97,10 +99,30 @@ def register_test(name, func=None):
"Tests can only be registered before rendering the first template"
)
if name in _TESTS:
- raise ValueError(f"A test with name {name!r} was registered already")
+ raise ValueError(f"A test with name {name!r} was already registered")
_TESTS[name] = func
return func
+def register_clever_function(name, func=None):
+ """Register a function to be available as test in templates under given name.
+
+ It can also be used as a decorator, see below in this module for examples.
+
+ :raise RuntimeError:
+ when trying to register a test after a template has been rendered already
+ :raise ValueError: when trying to register a name which was taken already
+ """
+ if func is None:
+ return functools.partial(register_clever_function, name)
+ if _get_environment.cache_info().currsize:
+ raise RuntimeError(
+ "Clever functions can only be registered before rendering the" \
+ "first template")
+ if name in _CLEVER_FUNCTIONS:
+ raise ValueError(f"A clever function with name {name!r} was already "\
+ "registered")
+ _CLEVER_FUNCTIONS[name] = func
+ return func
def render_to_string(template, content, formater=None, location=None):
"""Render a template from the template directory, raise on any errors.
@@ -150,6 +172,8 @@ def render(
# As we are opening the file with 'w', we are performing the rendering before
# calling open() to not accidentally erase the file if rendering fails
rendered = render_to_string(template, content, formater, location)
+ # Remove any trailing character and always add a new line at the end
+ rendered = rendered.rstrip() + "\n"
# Write to file
with open(destination, "w") as file:
@@ -390,28 +414,6 @@ def compare_netmask(netmask1, netmask2):
except:
return False
-@register_filter('isc_static_route')
-def isc_static_route(subnet, router):
- # https://ercpe.de/blog/pushing-static-routes-with-isc-dhcp-server
- # Option format is:
- # <netmask>, <network-byte1>, <network-byte2>, <network-byte3>, <router-byte1>, <router-byte2>, <router-byte3>
- # where bytes with the value 0 are omitted.
- from ipaddress import ip_network
- net = ip_network(subnet)
- # add netmask
- string = str(net.prefixlen) + ','
- # add network bytes
- if net.prefixlen:
- width = net.prefixlen // 8
- if net.prefixlen % 8:
- width += 1
- string += ','.join(map(str,tuple(net.network_address.packed)[:width])) + ','
-
- # add router bytes
- string += ','.join(router.split('.'))
-
- return string
-
@register_filter('is_file')
def is_file(filename):
if os.path.exists(filename):
@@ -612,12 +614,17 @@ def nft_default_rule(fw_conf, fw_name, family):
return " ".join(output)
@register_filter('nft_state_policy')
-def nft_state_policy(conf, state):
+def nft_state_policy(conf, state, bridge=False):
out = [f'ct state {state}']
+ action = conf['action'] if 'action' in conf else None
+
+ if bridge and action == 'reject':
+ action = 'drop' # T7148 - Bridge cannot use reject
+
if 'log' in conf:
log_state = state[:3].upper()
- log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper()
+ log_action = (action if action else 'accept')[:1].upper()
out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"')
if 'log_level' in conf:
@@ -626,8 +633,8 @@ def nft_state_policy(conf, state):
out.append('counter')
- if 'action' in conf:
- out.append(conf['action'])
+ if action:
+ out.append(action)
return " ".join(out)
@@ -779,6 +786,11 @@ def conntrack_ct_policy(protocol_conf):
return ", ".join(output)
+@register_filter('wlb_nft_rule')
+def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
+ from vyos.wanloadbalance import nft_rule as wlb_nft_rule
+ return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark)
+
@register_filter('range_to_regex')
def range_to_regex(num_range):
"""Convert range of numbers or list of ranges
@@ -871,10 +883,77 @@ def kea_high_availability_json(config):
return dumps(data)
+@register_filter('kea_dynamic_dns_update_main_json')
+def kea_dynamic_dns_update_main_json(config):
+ from vyos.kea import kea_parse_ddns_settings
+ from json import dumps
+
+ data = kea_parse_ddns_settings(config)
+
+ if len(data) == 0:
+ return ''
+
+ return dumps(data, indent=8)[1:-1] + ','
+
+@register_filter('kea_dynamic_dns_update_tsig_key_json')
+def kea_dynamic_dns_update_tsig_key_json(config):
+ from vyos.kea import kea_parse_tsig_algo
+ from json import dumps
+ out = []
+
+ if 'tsig_key' not in config:
+ return dumps(out)
+
+ tsig_keys = config['tsig_key']
+
+ for tsig_key_name, tsig_key_config in tsig_keys.items():
+ tsig_key = {
+ 'name': tsig_key_name,
+ 'algorithm': kea_parse_tsig_algo(tsig_key_config['algorithm']),
+ 'secret': tsig_key_config['secret']
+ }
+ out.append(tsig_key)
+
+ return dumps(out, indent=12)
+
+@register_filter('kea_dynamic_dns_update_domains')
+def kea_dynamic_dns_update_domains(config, type_key):
+ from json import dumps
+ out = []
+
+ if type_key not in config:
+ return dumps(out)
+
+ domains = config[type_key]
+
+ for domain_name, domain_config in domains.items():
+ domain = {
+ 'name': domain_name,
+
+ }
+ if 'key_name' in domain_config:
+ domain['key-name'] = domain_config['key_name']
+
+ if 'dns_server' in domain_config:
+ dns_servers = []
+ for dns_server_config in domain_config['dns_server'].values():
+ dns_server = {
+ 'ip-address': dns_server_config['address']
+ }
+ if 'port' in dns_server_config:
+ dns_server['port'] = int(dns_server_config['port'])
+ dns_servers.append(dns_server)
+ domain['dns-servers'] = dns_servers
+
+ out.append(domain)
+
+ return dumps(out, indent=12)
+
@register_filter('kea_shared_network_json')
def kea_shared_network_json(shared_networks):
from vyos.kea import kea_parse_options
from vyos.kea import kea_parse_subnet
+ from vyos.kea import kea_parse_ddns_settings
from json import dumps
out = []
@@ -885,9 +964,13 @@ def kea_shared_network_json(shared_networks):
network = {
'name': name,
'authoritative': ('authoritative' in config),
- 'subnet4': []
+ 'subnet4': [],
+ 'user-context': {}
}
+ if 'dynamic_dns_update' in config:
+ network.update(kea_parse_ddns_settings(config['dynamic_dns_update']))
+
if 'option' in config:
network['option-data'] = kea_parse_options(config['option'])
@@ -897,6 +980,9 @@ def kea_shared_network_json(shared_networks):
if 'bootfile_server' in config['option']:
network['next-server'] = config['option']['bootfile_server']
+ if 'ping_check' in config:
+ network['user-context']['enable-ping-check'] = True
+
if 'subnet' in config:
for subnet, subnet_config in config['subnet'].items():
if 'disable' in subnet_config:
@@ -988,3 +1074,21 @@ def vyos_defined(value, test_value=None, var_type=None):
else:
# Valid value and is matching optional argument if provided - return true
return True
+
+@register_clever_function('get_default_port')
+def get_default_port(service):
+ """
+ Jinja2 plugin to retrieve common service port number from vyos.defaults
+ class form a Jinja2 template. This removes the need to hardcode, or pass in
+ the data using the general dictionary.
+
+ Added to remove code complexity and make it easier to read.
+
+ Example:
+ {{ get_default_port('certbot_haproxy') }}
+ """
+ from vyos.defaults import internal_ports
+ if service not in internal_ports:
+ raise RuntimeError(f'Service "{service}" not found in internal ' \
+ 'vyos.defaults.internal_ports dict!')
+ return internal_ports[service]
diff --git a/python/vyos/utils/auth.py b/python/vyos/utils/auth.py
index a0b3e1cae..5d0e3464a 100644
--- a/python/vyos/utils/auth.py
+++ b/python/vyos/utils/auth.py
@@ -13,10 +13,80 @@
# You should have received a copy of the GNU Lesser General Public License along with this library;
# if not, write to the Free Software Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
+import cracklib
+import math
import re
+import string
+from enum import StrEnum
+from decimal import Decimal
from vyos.utils.process import cmd
+
+DEFAULT_PASSWORD: str = 'vyos'
+LOW_ENTROPY_MSG: str = 'should be at least 8 characters long;'
+WEAK_PASSWORD_MSG: str = 'The password complexity is too low - @MSG@'
+CRACKLIB_ERROR_MSG: str = 'A following error occurred: @MSG@\n' \
+ 'Possibly the cracklib database is corrupted or is missing. ' \
+ 'Try reinstalling the python3-cracklib package.'
+
+class EPasswdStrength(StrEnum):
+ WEAK = 'Weak'
+ DECENT = 'Decent'
+ STRONG = 'Strong'
+ ERROR = 'Cracklib Error'
+
+
+def calculate_entropy(charset: str, passwd: str) -> float:
+ """
+ Calculate the entropy of a password based on the set of characters used
+ Uses E = log2(R**L) formula, where
+ - R is the range (length) of the character set
+ - L is the length of password
+ """
+ return math.log(math.pow(len(charset), len(passwd)), 2)
+
+def evaluate_strength(passwd: str) -> dict[str, str]:
+ """ Evaluates password strength and returns a check result dict """
+ charset = (cracklib.ASCII_UPPERCASE + cracklib.ASCII_LOWERCASE +
+ string.punctuation + string.digits)
+
+ result = {
+ 'strength': '',
+ 'error': '',
+ }
+
+ try:
+ cracklib.FascistCheck(passwd)
+ except ValueError as e:
+ # The password is vulnerable to dictionary attack no matter the entropy
+ if 'is' in str(e):
+ msg = str(e).replace('is', 'should not be')
+ else:
+ msg = f'should not be {e}'
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(error=WEAK_PASSWORD_MSG.replace('@MSG@', msg))
+ except Exception as e:
+ result.update(strength=EPasswdStrength.ERROR)
+ result.update(error=CRACKLIB_ERROR_MSG.replace('@MSG@', str(e)))
+ else:
+ # Now check the password's entropy
+ # Cast to Decimal for more precise rounding
+ entropy = Decimal.from_float(calculate_entropy(charset, passwd))
+
+ match round(entropy):
+ case e if e in range(0, 59):
+ result.update(strength=EPasswdStrength.WEAK)
+ result.update(
+ error=WEAK_PASSWORD_MSG.replace('@MSG@', LOW_ENTROPY_MSG)
+ )
+ case e if e in range(60, 119):
+ result.update(strength=EPasswdStrength.DECENT)
+ case e if e >= 120:
+ result.update(strength=EPasswdStrength.STRONG)
+
+ return result
+
def make_password_hash(password):
""" Makes a password hash for /etc/shadow using mkpasswd """
diff --git a/python/vyos/utils/cpu.py b/python/vyos/utils/cpu.py
index 3bea5ac12..8ace77d15 100644
--- a/python/vyos/utils/cpu.py
+++ b/python/vyos/utils/cpu.py
@@ -99,3 +99,18 @@ def get_core_count():
core_count += 1
return core_count
+
+
+def get_available_cpus():
+ """ List of cpus with ids that are available in the system
+ Uses 'lscpu' command
+
+ Returns: list[dict[str, str | int | bool]]: cpus details
+ """
+ import json
+
+ from vyos.utils.process import cmd
+
+ out = json.loads(cmd('lscpu --extended -b --json'))
+
+ return out['cpus']
diff --git a/python/vyos/utils/misc.py b/python/vyos/utils/misc.py
index ac8011b8d..d82655914 100644
--- a/python/vyos/utils/misc.py
+++ b/python/vyos/utils/misc.py
@@ -52,7 +52,7 @@ def install_into_config(conf, config_paths, override_prompt=True):
continue
try:
- cmd(f'/usr/libexec/vyos/vyconf/vy_set {path}')
+ cmd(f'/opt/vyatta/sbin/my_set {path}')
count += 1
except:
failed.append(path)
diff --git a/python/vyos/utils/network.py b/python/vyos/utils/network.py
index dc0c0a6d6..20b6a3c9e 100644
--- a/python/vyos/utils/network.py
+++ b/python/vyos/utils/network.py
@@ -256,40 +256,60 @@ def mac2eui64(mac, prefix=None):
except: # pylint: disable=bare-except
return
-def check_port_availability(ipaddress, port, protocol):
+def check_port_availability(address: str=None, port: int=0, protocol: str='tcp') -> bool:
"""
- Check if port is available and not used by any service
- Return False if a port is busy or IP address does not exists
+ Check if given port is available and not used by any service.
+
Should be used carefully for services that can start listening
dynamically, because IP address may be dynamic too
+
+ Args:
+ address: IPv4 or IPv6 address - if None, checks on all interfaces
+ port: TCP/UDP port number.
+
+
+ Returns:
+ False if a port is busy or IP address does not exists
+ True if a port is free and IP address exists
"""
- from socketserver import TCPServer, UDPServer
+ import socket
from ipaddress import ip_address
+ # treat None as "any address"
+ address = address or '::'
+
# verify arguments
try:
- ipaddress = ip_address(ipaddress).compressed
- except:
- raise ValueError(f'The {ipaddress} is not a valid IPv4 or IPv6 address')
+ address = ip_address(address).compressed
+ except ValueError:
+ raise ValueError(f'{address} is not a valid IPv4 or IPv6 address')
if port not in range(1, 65536):
- raise ValueError(f'The port number {port} is not in the 1-65535 range')
+ raise ValueError(f'Port {port} is not in range 1-65535')
if protocol not in ['tcp', 'udp']:
- raise ValueError(f'The protocol {protocol} is not supported. Only tcp and udp are allowed')
+ raise ValueError(f'{protocol} is not supported - only tcp and udp are allowed')
- # check port availability
+ protocol = socket.SOCK_STREAM if protocol == 'tcp' else socket.SOCK_DGRAM
try:
- if protocol == 'tcp':
- server = TCPServer((ipaddress, port), None, bind_and_activate=True)
- if protocol == 'udp':
- server = UDPServer((ipaddress, port), None, bind_and_activate=True)
- server.server_close()
- except Exception as e:
- # errno.h:
- #define EADDRINUSE 98 /* Address already in use */
- if e.errno == 98:
+ addr_info = socket.getaddrinfo(address, port, socket.AF_UNSPEC, protocol)
+ except socket.gaierror as e:
+ print(f'Invalid address: {address}')
+ return False
+
+ for family, socktype, proto, canonname, sockaddr in addr_info:
+ try:
+ with socket.socket(family, socktype, proto) as s:
+ s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
+ s.bind(sockaddr)
+ # port is free to use
+ return True
+ except OSError:
+ # port is already in use
return False
- return True
+ # if we reach this point, no socket was tested and we assume the port is
+ # already in use - better safe then sorry
+ return False
+
def is_listen_port_bind_service(port: int, service: str) -> bool:
"""Check if listen port bound to expected program name
@@ -599,3 +619,35 @@ def get_nft_vrf_zone_mapping() -> dict:
for (vrf_name, vrf_id) in vrf_list:
output.append({'interface' : vrf_name, 'vrf_tableid' : vrf_id})
return output
+
+def is_valid_ipv4_address_or_range(addr: str) -> bool:
+ """
+ Validates if the provided address is a valid IPv4, CIDR or IPv4 range
+ :param addr: address to test
+ :return: bool: True if provided address is valid
+ """
+ from ipaddress import ip_network
+ try:
+ if '-' in addr: # If we are checking a range, validate both address's individually
+ split = addr.split('-')
+ return is_valid_ipv4_address_or_range(split[0]) and is_valid_ipv4_address_or_range(split[1])
+ else:
+ return ip_network(addr).version == 4
+ except:
+ return False
+
+def is_valid_ipv6_address_or_range(addr: str) -> bool:
+ """
+ Validates if the provided address is a valid IPv4, CIDR or IPv4 range
+ :param addr: address to test
+ :return: bool: True if provided address is valid
+ """
+ from ipaddress import ip_network
+ try:
+ if '-' in addr: # If we are checking a range, validate both address's individually
+ split = addr.split('-')
+ return is_valid_ipv6_address_or_range(split[0]) and is_valid_ipv6_address_or_range(split[1])
+ else:
+ return ip_network(addr).version == 6
+ except:
+ return False
diff --git a/python/vyos/utils/process.py b/python/vyos/utils/process.py
index 054088325..21335e6b3 100644
--- a/python/vyos/utils/process.py
+++ b/python/vyos/utils/process.py
@@ -14,6 +14,7 @@
# License along with this library. If not, see <http://www.gnu.org/licenses/>.
import os
+import shlex
from subprocess import Popen
from subprocess import PIPE
@@ -21,20 +22,17 @@ from subprocess import STDOUT
from subprocess import DEVNULL
-def get_wrapper(vrf, netns, auth):
- wrapper = ''
+def get_wrapper(vrf, netns):
+ wrapper = None
if vrf:
- wrapper = f'ip vrf exec {vrf} '
+ wrapper = ['ip', 'vrf', 'exec', vrf]
elif netns:
- wrapper = f'ip netns exec {netns} '
- if auth:
- wrapper = f'{auth} {wrapper}'
+ wrapper = ['ip', 'netns', 'exec', netns]
return wrapper
def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
- stdout=PIPE, stderr=PIPE, decode='utf-8', auth='', vrf=None,
- netns=None):
+ stdout=PIPE, stderr=PIPE, decode='utf-8', vrf=None, netns=None):
"""
popen is a wrapper helper around subprocess.Popen
with it default setting it will return a tuple (out, err)
@@ -75,28 +73,33 @@ def popen(command, flag='', shell=None, input=None, timeout=None, env=None,
if not debug.enabled(flag):
flag = 'command'
+ use_shell = shell
+ stdin = None
+ if shell is None:
+ use_shell = False
+ if ' ' in command:
+ use_shell = True
+ if env:
+ use_shell = True
+
# Must be run as root to execute command in VRF or network namespace
+ wrapper = get_wrapper(vrf, netns)
if vrf or netns:
if os.getuid() != 0:
raise OSError(
'Permission denied: cannot execute commands in VRF and netns contexts as an unprivileged user'
)
- wrapper = get_wrapper(vrf, netns, auth)
- command = f'{wrapper} {command}'
+ if use_shell:
+ command = f'{shlex.join(wrapper)} {command}'
+ else:
+ if type(command) is not list:
+ command = [command]
+ command = wrapper + command
- cmd_msg = f"cmd '{command}'"
+ cmd_msg = f"cmd '{command}'" if use_shell else f"cmd '{shlex.join(command)}'"
debug.message(cmd_msg, flag)
- use_shell = shell
- stdin = None
- if shell is None:
- use_shell = False
- if ' ' in command:
- use_shell = True
- if env:
- use_shell = True
-
if input:
stdin = PIPE
input = input.encode() if type(input) is str else input
@@ -155,7 +158,7 @@ def run(command, flag='', shell=None, input=None, timeout=None, env=None,
def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
stdout=PIPE, stderr=PIPE, decode='utf-8', raising=None, message='',
- expect=[0], auth='', vrf=None, netns=None):
+ expect=[0], vrf=None, netns=None):
"""
A wrapper around popen, which returns the stdout and
will raise the error code of a command
@@ -171,12 +174,11 @@ def cmd(command, flag='', shell=None, input=None, timeout=None, env=None,
input=input, timeout=timeout,
env=env, shell=shell,
decode=decode,
- auth=auth,
vrf=vrf,
netns=netns,
)
if code not in expect:
- wrapper = get_wrapper(vrf, netns, auth='')
+ wrapper = get_wrapper(vrf, netns)
command = f'{wrapper} {command}'
feedback = message + '\n' if message else ''
feedback += f'failed to run command: {command}\n'
diff --git a/python/vyos/vyconf_session.py b/python/vyos/vyconf_session.py
new file mode 100644
index 000000000..506095625
--- /dev/null
+++ b/python/vyos/vyconf_session.py
@@ -0,0 +1,123 @@
+# Copyright 2025 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/>.
+#
+#
+
+import tempfile
+import shutil
+from functools import wraps
+from typing import Type
+
+from vyos.proto import vyconf_client
+from vyos.migrate import ConfigMigrate
+from vyos.migrate import ConfigMigrateError
+from vyos.component_version import append_system_version
+
+
+def output(o):
+ out = ''
+ for res in (o.output, o.error, o.warning):
+ if res is not None:
+ out = out + res
+ return out
+
+
+class VyconfSession:
+ def __init__(self, token: str = None, on_error: Type[Exception] = None):
+ if token is None:
+ out = vyconf_client.send_request('setup_session')
+ self.__token = out.output
+ else:
+ self.__token = token
+
+ self.on_error = on_error
+
+ @staticmethod
+ def raise_exception(f):
+ @wraps(f)
+ def wrapped(self, *args, **kwargs):
+ if self.on_error is None:
+ return f(self, *args, **kwargs)
+ o, e = f(self, *args, **kwargs)
+ if e:
+ raise self.on_error(o)
+ return o, e
+
+ return wrapped
+
+ @raise_exception
+ def set(self, path: list[str]) -> tuple[str, int]:
+ out = vyconf_client.send_request('set', token=self.__token, path=path)
+ return output(out), out.status
+
+ @raise_exception
+ def delete(self, path: list[str]) -> tuple[str, int]:
+ out = vyconf_client.send_request('delete', token=self.__token, path=path)
+ return output(out), out.status
+
+ @raise_exception
+ def commit(self) -> tuple[str, int]:
+ out = vyconf_client.send_request('commit', token=self.__token)
+ return output(out), out.status
+
+ @raise_exception
+ def discard(self) -> tuple[str, int]:
+ out = vyconf_client.send_request('discard', token=self.__token)
+ return output(out), out.status
+
+ def session_changed(self) -> bool:
+ out = vyconf_client.send_request('session_changed', token=self.__token)
+ return not bool(out.status)
+
+ @raise_exception
+ def load_config(self, file: str, migrate: bool = False) -> tuple[str, int]:
+ # pylint: disable=consider-using-with
+ if migrate:
+ tmp = tempfile.NamedTemporaryFile()
+ shutil.copy2(file, tmp.name)
+ config_migrate = ConfigMigrate(tmp.name)
+ try:
+ config_migrate.run()
+ except ConfigMigrateError as e:
+ tmp.close()
+ return repr(e), 1
+ file = tmp.name
+ else:
+ tmp = ''
+
+ out = vyconf_client.send_request('load', token=self.__token, location=file)
+ if tmp:
+ tmp.close()
+
+ return output(out), out.status
+
+ @raise_exception
+ def save_config(self, file: str, append_version: bool = False) -> tuple[str, int]:
+ out = vyconf_client.send_request('save', token=self.__token, location=file)
+ if append_version:
+ append_system_version(file)
+ return output(out), out.status
+
+ @raise_exception
+ def show_config(self, path: list[str] = None) -> tuple[str, int]:
+ if path is None:
+ path = []
+ out = vyconf_client.send_request('show_config', token=self.__token, path=path)
+ return output(out), out.status
+
+ def __del__(self):
+ out = vyconf_client.send_request('teardown', token=self.__token)
+ if out.status:
+ print(f'Could not tear down session {self.__token}: {output(out)}')
diff --git a/python/vyos/wanloadbalance.py b/python/vyos/wanloadbalance.py
new file mode 100644
index 000000000..62e109f21
--- /dev/null
+++ b/python/vyos/wanloadbalance.py
@@ -0,0 +1,153 @@
+#!/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 os
+
+from vyos.defaults import directories
+from vyos.utils.process import run
+
+dhclient_lease = 'dhclient_{0}.lease'
+
+def nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False):
+ output = []
+
+ if 'inbound_interface' in rule_conf:
+ ifname = rule_conf['inbound_interface']
+ if local and not exclude:
+ output.append(f'oifname != "{ifname}"')
+ elif not local:
+ output.append(f'iifname "{ifname}"')
+
+ if 'protocol' in rule_conf and rule_conf['protocol'] != 'all':
+ protocol = rule_conf['protocol']
+ operator = ''
+
+ if protocol[:1] == '!':
+ operator = '!='
+ protocol = protocol[1:]
+
+ if protocol == 'tcp_udp':
+ protocol = '{ tcp, udp }'
+
+ output.append(f'meta l4proto {operator} {protocol}')
+
+ for direction in ['source', 'destination']:
+ if direction not in rule_conf:
+ continue
+
+ direction_conf = rule_conf[direction]
+ prefix = direction[:1]
+
+ if 'address' in direction_conf:
+ operator = ''
+ address = direction_conf['address']
+ if address[:1] == '!':
+ operator = '!='
+ address = address[1:]
+ output.append(f'ip {prefix}addr {operator} {address}')
+
+ if 'port' in direction_conf:
+ operator = ''
+ port = direction_conf['port']
+ if port[:1] == '!':
+ operator = '!='
+ port = port[1:]
+ output.append(f'th {prefix}port {operator} {port}')
+
+ if 'source_based_routing' not in rule_conf and not restore_mark:
+ output.append('ct state new')
+
+ if limit and 'limit' in rule_conf and 'rate' in rule_conf['limit']:
+ output.append(f'limit rate {rule_conf["limit"]["rate"]}/{rule_conf["limit"]["period"]}')
+ if 'burst' in rule_conf['limit']:
+ output.append(f'burst {rule_conf["limit"]["burst"]} packets')
+
+ output.append('counter')
+
+ if restore_mark:
+ output.append('meta mark set ct mark')
+ elif weight:
+ weights, total_weight = wlb_weight_interfaces(rule_conf, health_state)
+ if len(weights) > 1: # Create weight-based verdict map
+ vmap_str = ", ".join(f'{weight} : jump wlb_mangle_isp_{ifname}' for ifname, weight in weights)
+ output.append(f'numgen random mod {total_weight} vmap {{ {vmap_str} }}')
+ elif len(weights) == 1: # Jump to single ISP
+ ifname, _ = weights[0]
+ output.append(f'jump wlb_mangle_isp_{ifname}')
+ else: # No healthy interfaces
+ return ""
+ elif action:
+ output.append(action)
+
+ return " ".join(output)
+
+def wlb_weight_interfaces(rule_conf, health_state):
+ interfaces = []
+
+ for ifname, if_conf in rule_conf['interface'].items():
+ if ifname in health_state and health_state[ifname]['state']:
+ weight = int(if_conf.get('weight', 1))
+ interfaces.append((ifname, weight))
+
+ if not interfaces:
+ return [], 0
+
+ if 'failover' in rule_conf:
+ for ifpair in sorted(interfaces, key=lambda i: i[1], reverse=True):
+ return [ifpair], ifpair[1] # Return highest weight interface that is ACTIVE when in failover
+
+ total_weight = sum(weight for _, weight in interfaces)
+ out = []
+ start = 0
+ for ifname, weight in sorted(interfaces, key=lambda i: i[1]): # build weight ranges
+ end = start + weight - 1
+ out.append((ifname, f'{start}-{end}' if end > start else start))
+ start = weight
+
+ return out, total_weight
+
+def health_ping_host(host, ifname, count=1, wait_time=0):
+ cmd_str = f'ping -c {count} -W {wait_time} -I {ifname} {host}'
+ rc = run(cmd_str)
+ return rc == 0
+
+def health_ping_host_ttl(host, ifname, count=1, ttl_limit=0):
+ cmd_str = f'ping -c {count} -t {ttl_limit} -I {ifname} {host}'
+ rc = run(cmd_str)
+ return rc != 0
+
+def parse_dhcp_nexthop(ifname):
+ lease_file = os.path.join(directories['isc_dhclient_dir'], dhclient_lease.format(ifname))
+
+ if not os.path.exists(lease_file):
+ return False
+
+ with open(lease_file, 'r') as f:
+ for line in f.readlines():
+ data = line.replace('\n', '').split('=')
+ if data[0] == 'new_routers':
+ return data[1].replace("'", '').split(" ")[0]
+
+ return None
+
+def parse_ppp_nexthop(ifname):
+ nexthop_file = os.path.join(directories['ppp_nexthop_dir'], ifname)
+
+ if not os.path.exists(nexthop_file):
+ return False
+
+ with open(nexthop_file, 'r') as f:
+ return f.read()
diff --git a/scripts/build-command-op-templates b/scripts/build-command-op-templates
index d203fdcef..0bb62113e 100755
--- a/scripts/build-command-op-templates
+++ b/scripts/build-command-op-templates
@@ -116,7 +116,7 @@ def get_properties(p):
if comptype is not None:
props["comp_type"] = "imagefiles"
comp_exprs.append("echo -n \"<imagefiles>\"")
- comp_help = " && ".join(comp_exprs)
+ comp_help = " ; ".join(comp_exprs)
props["comp_help"] = comp_help
except:
diff --git a/smoketest/config-tests/basic-api-service b/smoketest/config-tests/basic-api-service
index 3f796f35d..ca10cf4e9 100644
--- a/smoketest/config-tests/basic-api-service
+++ b/smoketest/config-tests/basic-api-service
@@ -24,5 +24,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/basic-haproxy b/smoketest/config-tests/basic-haproxy
new file mode 100644
index 000000000..7755fc4ea
--- /dev/null
+++ b/smoketest/config-tests/basic-haproxy
@@ -0,0 +1,46 @@
+set interfaces dummy dum0 address '172.18.254.203/32'
+set interfaces ethernet eth0 duplex 'auto'
+set interfaces ethernet eth0 speed 'auto'
+set interfaces ethernet eth0 vif 203 address '172.18.203.10/24'
+set interfaces ethernet eth1 duplex 'auto'
+set interfaces ethernet eth1 speed 'auto'
+set interfaces ethernet eth2 duplex 'auto'
+set interfaces ethernet eth2 speed 'auto'
+set load-balancing haproxy backend webserver logging facility daemon
+set load-balancing haproxy backend webserver logging facility user level 'info'
+set load-balancing haproxy backend webserver server web01 address '192.0.2.1'
+set load-balancing haproxy backend webserver server web01 port '443'
+set load-balancing haproxy backend webserver ssl no-verify
+set load-balancing haproxy global-parameters logging facility daemon
+set load-balancing haproxy global-parameters logging facility user level 'info'
+set load-balancing haproxy service frontend backend 'webserver'
+set load-balancing haproxy service frontend logging facility daemon
+set load-balancing haproxy service frontend logging facility user level 'info'
+set load-balancing haproxy service frontend port '443'
+set load-balancing haproxy service frontend ssl certificate 'dummy'
+set pki certificate dummy certificate 'MIIDsTCCApmgAwIBAgIUegVgO1wIN2v44trXZ+Kb1t48uL0wDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNTA1MDUxODIzMTdaFw0yNjA1MDUxODIzMTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSr4OYIWkb8UGuQnEFnjSbmvR+4vDAfBgNVHSMEGDAWgBSr4OYIWkb8UGuQnEFnjSbmvR+4vDANBgkqhkiG9w0BAQsFAAOCAQEAUmRWRPGXsvfuRT+53id3EufH1IJAdowrt6yBZsHobvqCXO2+YhG7oG6/UqUYiv5bHN5xEMQyWd7nyrLOUeFo2bpcMIOlpl6AoUIY65Gm2BqQ7FuPxLLO25RdpZ5WkMGX5kJsKY0/PcpamRKNz1khgFcRyxf9WGhCAIjDCWIWs8lkvPN3m75SFCW7MTuzzQOrzvI6nqqcHO4k8hRBznp26WLUW1rQKpNN09nZGOkeNYK5QbzKN/RUmtEHQZhlgLAIr09jUaA4RDLI1SdD6LR5nvpa9RJBTyS/kISF8BXKMgvUbDHN2nP+VUUrut2ZwoU+pxV4RVT2pS760HuYj4+sYQ=='
+set pki certificate dummy private key 'MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAECgf9plqCMg2pKEWRFl183bqWAm7lnLnsUOfABFNPYa3U+uKQUKZpboTBfzDfZqNak3XNQV0mTAR8pFfoMhQjQU9hUxH7ivjw1RUCHjixCF0vLBkTB34gL7FUbiEIFhR1NW3pCJY73OXOkIZG1Obh6Syb8KubDeu4bTmb90/TnDDAs6OYXJ5yo7ZDZLvLu5a3Dli+H4K5Qb5VJ74o/vtodBo3wmKBgy2Ey6JqF+y7/3HeE66rVhYNft5pURgemWnNYqh3oDTJASqpA/8n90o8ceYPVJugQ7029UiyTp0xgBRXFszgiYPkBlsNWB+9+ospopOmYU0owBykH+RtD/bQ0mwECgYEA4gxPdYbHg/GLihHrkv0t5V0pSEhBeBeTBdWG+P/6K90vXofpp7qISdOeYMGkh8mY+PfZNHksdu67d2ks5on5/dNf5YXWCm+LMMRiSsfOo11NISNdNHS6afqs18Wq1aKawv/rwotfxrakM4Gar692+jgz/l/X+FdOQwmE6uEus8ECgYEA3oW4eGtzGq28TiqyhTHduTkas0ckPWX8ulasyPnLxBKDNNohbXXpFIJIcrnl24QFJw3MJbo+R+OxZHgzPK8r64gIGVa7vLCR2fiU/RFoUa1Jo1pPOqXXWqf/Mvdokm2p0atrjRUX9VhjoFsDLcqTgAmfSCsVSqGucX6ER7Gy60UCgYAvmhoNjNFtFquk6rsqHAjTOTgdUaH/0S8T1nBy9SzQmeaEyKhKuvxCV78NbxnfwnNlUoQ6CZ50eTefINXkwn+TlTSnl/SIBA9SuLheOQ9p1ZcNeG4DQuWStcg6NBUSoghnMg+Ky2Di7slLU2qovpGWhcllMve/A1umwFVuRPdZwQKBgHS7mYYyd/Oq6HnpFDWjbzlXp5Yc3/oFooruJT5ZLHfzbjkvpRGTJW7I2dC1jMuXekx+hHXWOg3keI7IL7jJ/DRW7Ei+o0XdKuY57Y7ErwEJ8vNq0N1nWo4IS2wlNgp61PdVAdrFEgh3EexxUj2XY8FrSs/FKio4nxaS1Dn4EnAxAoGBAKqLvuPpmCMVwlIu57WGxL5d9i7EjIGY65l6HTKQYoHCzE51rkowH1La2fuUYz0IpExq2lcrLbOUtSyhXH7Zlktiz//Gu/P90SUfR/ZGcLeZi+EDyK2OctpnWBjs2Dmfg4D6vxk39yV8AB97pYG073GcJ/P54qRUuEitbpJwH+fB'
+set service ntp allow-client address '0.0.0.0/0'
+set service ntp allow-client address '::/0'
+set service ntp server 172.16.100.10
+set service ntp server 172.16.100.20
+set service ntp server 172.16.110.30
+set service ssh disable-host-validation
+set service ssh port '22'
+set system config-management commit-revisions '200'
+set system conntrack modules ftp
+set system conntrack modules h323
+set system conntrack modules nfs
+set system conntrack modules pptp
+set system conntrack modules sip
+set system conntrack modules sqlnet
+set system conntrack modules tftp
+set system console device ttyS0 speed '115200'
+set system host-name 'vyos'
+set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0'
+set system login user vyos authentication plaintext-password ''
+set system name-server '172.16.254.30'
+set system option kernel disable-mitigations
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
+set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/basic-syslog b/smoketest/config-tests/basic-syslog
new file mode 100644
index 000000000..349d642fd
--- /dev/null
+++ b/smoketest/config-tests/basic-syslog
@@ -0,0 +1,25 @@
+set interfaces ethernet eth0 duplex 'auto'
+set interfaces ethernet eth0 speed 'auto'
+set interfaces ethernet eth1 address '172.16.33.154/24'
+set interfaces ethernet eth1 duplex 'auto'
+set interfaces ethernet eth1 speed 'auto'
+set interfaces ethernet eth1 vrf 'red'
+set system console device ttyS0 speed '115200'
+set system domain-name 'vyos-ci-test.net'
+set system host-name 'vyos'
+set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0'
+set system login user vyos authentication plaintext-password ''
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
+set system syslog marker interval '999'
+set system syslog preserve-fqdn
+set system syslog remote syslog01.vyos.net facility local7 level 'notice'
+set system syslog remote syslog01.vyos.net port '8000'
+set system syslog remote syslog01.vyos.net vrf 'red'
+set system syslog remote syslog02.vyos.net facility all level 'debug'
+set system syslog remote syslog02.vyos.net format include-timezone
+set system syslog remote syslog02.vyos.net format octet-counted
+set system syslog remote syslog02.vyos.net port '8001'
+set system syslog remote syslog02.vyos.net protocol 'tcp'
+set system syslog remote syslog02.vyos.net vrf 'red'
+set vrf name red table '12321'
diff --git a/smoketest/config-tests/basic-vyos b/smoketest/config-tests/basic-vyos
index 6ff28ec2e..aaf450e80 100644
--- a/smoketest/config-tests/basic-vyos
+++ b/smoketest/config-tests/basic-vyos
@@ -28,7 +28,21 @@ set protocols static arp interface eth2.200.201 address 100.64.201.20 mac '00:50
set protocols static arp interface eth2.200.202 address 100.64.202.30 mac '00:50:00:00:00:30'
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 dynamic-dns-update send-updates 'enable'
+set service dhcp-server dynamic-dns-update conflict-resolution 'enable'
+set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates algorithm 'sha256'
+set service dhcp-server dynamic-dns-update tsig-key domain-lan-updates secret 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='
+set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 algorithm 'sha256'
+set service dhcp-server dynamic-dns-update tsig-key reverse-0-168-192 secret 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='
+set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 1 address '192.168.0.1'
+set service dhcp-server dynamic-dns-update forward-domain domain.lan dns-server 2 address '100.100.0.1'
+set service dhcp-server dynamic-dns-update forward-domain domain.lan key-name 'domain-lan-updates'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 1 address '192.168.0.1'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa dns-server 2 address '100.100.0.1'
+set service dhcp-server dynamic-dns-update reverse-domain 0.168.192.in-addr.arpa key-name 'reverse-0-168-192'
set service dhcp-server shared-network-name LAN authoritative
+set service dhcp-server shared-network-name LAN dynamic-dns-update send-updates 'enable'
+set service dhcp-server shared-network-name LAN dynamic-dns-update ttl-percent '75'
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'
@@ -46,6 +60,9 @@ 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 TEST2-2 ip-address '192.168.0.21'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 static-mapping TEST2-2 mac '00:01:02:03:04:22'
set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 subnet-id '1'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update send-updates 'enable'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update generated-prefix 'myhost'
+set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 dynamic-dns-update qualifying-suffix 'lan1.domain.lan'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 interface 'eth0'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option domain-search 'vyos.net'
set service dhcpv6-server shared-network-name LAN6 subnet fe88::/56 option name-server 'fe88::1'
@@ -92,12 +109,14 @@ set system login user vyos authentication plaintext-password ''
set system name-server '192.168.0.1'
set system syslog console facility all level 'emerg'
set system syslog console facility mail level 'info'
-set system syslog global facility all level 'info'
-set system syslog global facility auth level 'info'
-set system syslog global facility local7 level 'debug'
-set system syslog global preserve-fqdn
-set system syslog host syslog.vyos.net facility auth level 'warning'
-set system syslog host syslog.vyos.net facility local7 level 'notice'
-set system syslog host syslog.vyos.net format octet-counted
-set system syslog host syslog.vyos.net port '8000'
+set system syslog local facility all level 'info'
+set system syslog local facility auth level 'info'
+set system syslog local facility local7 level 'debug'
+set system syslog marker interval '1000'
+set system syslog preserve-fqdn
+set system syslog remote syslog.vyos.net facility auth level 'warning'
+set system syslog remote syslog.vyos.net facility local7 level 'notice'
+set system syslog remote syslog.vyos.net format octet-counted
+set system syslog remote syslog.vyos.net port '8000'
+set system syslog remote syslog.vyos.net protocol 'tcp'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/basic-vyos-no-ntp b/smoketest/config-tests/basic-vyos-no-ntp
index a18260108..f00dea5d4 100644
--- a/smoketest/config-tests/basic-vyos-no-ntp
+++ b/smoketest/config-tests/basic-vyos-no-ntp
@@ -48,6 +48,6 @@ set system host-name 'no-ntp'
set system login user vyos authentication encrypted-password '$6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0'
set system login user vyos authentication plaintext-password ''
set system name-server '172.16.254.30'
-set system syslog global facility all level 'debug'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'debug'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/bgp-azure-ipsec-gateway b/smoketest/config-tests/bgp-azure-ipsec-gateway
index bbd7b961f..0d683c921 100644
--- a/smoketest/config-tests/bgp-azure-ipsec-gateway
+++ b/smoketest/config-tests/bgp-azure-ipsec-gateway
@@ -135,10 +135,10 @@ set system login user vyos authentication plaintext-password ''
set system logs logrotate messages max-size '20'
set system logs logrotate messages rotate '10'
set system name-server '192.0.2.254'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
-set system syslog host 10.0.9.188 facility all level 'info'
-set system syslog host 10.0.9.188 protocol 'udp'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
+set system syslog remote 10.0.9.188 facility all level 'info'
+set system syslog remote 10.0.9.188 protocol 'udp'
set system time-zone 'Europe/Berlin'
set vpn ipsec authentication psk peer_51-105-0-1 id '51.105.0.1'
set vpn ipsec authentication psk peer_51-105-0-1 id '192.0.2.189'
diff --git a/smoketest/config-tests/bgp-bfd-communities b/smoketest/config-tests/bgp-bfd-communities
index 6eee0137e..06e412c55 100644
--- a/smoketest/config-tests/bgp-bfd-communities
+++ b/smoketest/config-tests/bgp-bfd-communities
@@ -196,6 +196,6 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/bgp-big-as-cloud b/smoketest/config-tests/bgp-big-as-cloud
index d6c17b3d2..f71a51be3 100644
--- a/smoketest/config-tests/bgp-big-as-cloud
+++ b/smoketest/config-tests/bgp-big-as-cloud
@@ -847,6 +847,6 @@ set system name-server '192.0.2.2'
set system sflow interface 'eth0.4088'
set system sflow interface 'eth0.4089'
set system sflow server 1.2.3.4 port '1234'
-set system syslog global facility all level 'all'
-set system syslog global preserve-fqdn
+set system syslog local facility all level 'all'
+set system syslog preserve-fqdn
set system time-zone 'Europe/Zurich'
diff --git a/smoketest/config-tests/bgp-dmvpn-hub b/smoketest/config-tests/bgp-dmvpn-hub
index 99f3799a4..f9ceba11c 100644
--- a/smoketest/config-tests/bgp-dmvpn-hub
+++ b/smoketest/config-tests/bgp-dmvpn-hub
@@ -50,8 +50,8 @@ set system login user vyos authentication plaintext-password ''
set system name-server '1.1.1.1'
set system name-server '8.8.8.8'
set system name-server '9.9.9.9'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vpn ipsec esp-group ESP-DMVPN lifetime '1800'
set vpn ipsec esp-group ESP-DMVPN mode 'transport'
set vpn ipsec esp-group ESP-DMVPN pfs 'dh-group2'
diff --git a/smoketest/config-tests/bgp-dmvpn-spoke b/smoketest/config-tests/bgp-dmvpn-spoke
index e4fb82a0e..a98275ba4 100644
--- a/smoketest/config-tests/bgp-dmvpn-spoke
+++ b/smoketest/config-tests/bgp-dmvpn-spoke
@@ -56,8 +56,8 @@ set system login user vyos authentication plaintext-password ''
set system name-server '1.1.1.1'
set system name-server '8.8.8.8'
set system name-server '9.9.9.9'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vpn ipsec esp-group ESP-DMVPN lifetime '1800'
set vpn ipsec esp-group ESP-DMVPN mode 'transport'
set vpn ipsec esp-group ESP-DMVPN pfs 'dh-group2'
diff --git a/smoketest/config-tests/bgp-evpn-l2vpn-leaf b/smoketest/config-tests/bgp-evpn-l2vpn-leaf
index 315cb9e06..5e42a269e 100644
--- a/smoketest/config-tests/bgp-evpn-l2vpn-leaf
+++ b/smoketest/config-tests/bgp-evpn-l2vpn-leaf
@@ -48,8 +48,8 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vrf name MGMT protocols static route 0.0.0.0/0 next-hop 192.0.2.62
set vrf name MGMT protocols static route6 ::/0 next-hop 2001:db8::1
set vrf name MGMT table '1000'
diff --git a/smoketest/config-tests/bgp-evpn-l2vpn-spine b/smoketest/config-tests/bgp-evpn-l2vpn-spine
index dee29e021..e6d876af6 100644
--- a/smoketest/config-tests/bgp-evpn-l2vpn-spine
+++ b/smoketest/config-tests/bgp-evpn-l2vpn-spine
@@ -41,8 +41,8 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vrf name MGMT protocols static route 0.0.0.0/0 next-hop 192.0.2.62
set vrf name MGMT protocols static route6 ::/0 next-hop 2001:db8::1
set vrf name MGMT table '1000'
diff --git a/smoketest/config-tests/bgp-evpn-l3vpn-pe-router b/smoketest/config-tests/bgp-evpn-l3vpn-pe-router
index 7a2ec9f91..f867c221e 100644
--- a/smoketest/config-tests/bgp-evpn-l3vpn-pe-router
+++ b/smoketest/config-tests/bgp-evpn-l3vpn-pe-router
@@ -101,8 +101,8 @@ set system login user vyos authentication plaintext-password ''
set system name-server '192.0.2.251'
set system name-server '192.0.2.252'
set system name-server '2001:db8::1'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vrf name blue protocols bgp address-family ipv4-unicast redistribute connected
set vrf name blue protocols bgp address-family l2vpn-evpn advertise ipv4 unicast
set vrf name blue protocols bgp system-as '100'
diff --git a/smoketest/config-tests/bgp-medium-confederation b/smoketest/config-tests/bgp-medium-confederation
index 582e28047..71797fe93 100644
--- a/smoketest/config-tests/bgp-medium-confederation
+++ b/smoketest/config-tests/bgp-medium-confederation
@@ -69,5 +69,5 @@ 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'
+set system syslog local facility all level 'notice'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/bgp-rpki b/smoketest/config-tests/bgp-rpki
index 44e95ae98..657d4abcc 100644
--- a/smoketest/config-tests/bgp-rpki
+++ b/smoketest/config-tests/bgp-rpki
@@ -13,6 +13,7 @@ set policy route-map ebgp-transit-rpki rule 30 set local-preference '100'
set policy route-map ebgp-transit-rpki rule 40 action 'permit'
set policy route-map ebgp-transit-rpki rule 40 set extcommunity rt '192.0.2.100:100'
set policy route-map ebgp-transit-rpki rule 40 set extcommunity soo '64500:100'
+set protocols bgp address-family ipv4-unicast redistribute table 100
set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast nexthop-self
set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast route-map import 'ebgp-transit-rpki'
set protocols bgp neighbor 1.2.3.4 remote-as '10'
@@ -39,5 +40,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/bgp-small-internet-exchange b/smoketest/config-tests/bgp-small-internet-exchange
index a9dce4dd5..2adb3fbb5 100644
--- a/smoketest/config-tests/bgp-small-internet-exchange
+++ b/smoketest/config-tests/bgp-small-internet-exchange
@@ -205,5 +205,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/bgp-small-ipv4-unicast b/smoketest/config-tests/bgp-small-ipv4-unicast
index b8c0e1246..f8820cb3c 100644
--- a/smoketest/config-tests/bgp-small-ipv4-unicast
+++ b/smoketest/config-tests/bgp-small-ipv4-unicast
@@ -28,5 +28,5 @@ set system domain-name 'vyos.net'
set system host-name 'vyos'
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'
+set system syslog local facility all level 'notice'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/cluster-basic b/smoketest/config-tests/cluster-basic
index 744c117eb..871b40bbb 100644
--- a/smoketest/config-tests/cluster-basic
+++ b/smoketest/config-tests/cluster-basic
@@ -16,6 +16,6 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Antarctica/South_Pole'
diff --git a/smoketest/config-tests/dialup-router-complex b/smoketest/config-tests/dialup-router-complex
index c693cc382..12edcfef2 100644
--- a/smoketest/config-tests/dialup-router-complex
+++ b/smoketest/config-tests/dialup-router-complex
@@ -695,6 +695,7 @@ set service dns forwarding ignore-hosts-file
set service dns forwarding listen-address '172.16.254.30'
set service dns forwarding listen-address '172.31.0.254'
set service dns forwarding negative-ttl '60'
+set service lldp interface pppoe0 mode 'disable'
set service lldp legacy-protocols cdp
set service lldp snmp
set service mdns repeater interface 'eth0.35'
@@ -734,7 +735,7 @@ set system name-server '172.16.254.30'
set system option ctrl-alt-delete 'ignore'
set system option reboot-on-panic
set system option startup-beep
-set system syslog global facility all level 'debug'
-set system syslog global facility local7 level 'debug'
-set system syslog host 172.16.100.1 facility all level 'warning'
+set system syslog local facility all level 'debug'
+set system syslog local facility local7 level 'debug'
+set system syslog remote 172.16.100.1 facility all level 'warning'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn
index d6b00c678..ba3ed29f4 100644
--- a/smoketest/config-tests/dialup-router-medium-vpn
+++ b/smoketest/config-tests/dialup-router-medium-vpn
@@ -314,9 +314,9 @@ set system static-host-mapping host-name host107.vyos.net inet '192.168.0.107'
set system static-host-mapping host-name host109.vyos.net inet '192.168.0.109'
set system sysctl parameter net.core.default_qdisc value 'fq'
set system sysctl parameter net.ipv4.tcp_congestion_control value 'bbr'
-set system syslog global facility all level 'info'
-set system syslog host 192.168.0.252 facility all level 'debug'
-set system syslog host 192.168.0.252 protocol 'udp'
+set system syslog local facility all level 'info'
+set system syslog remote 192.168.0.252 facility all level 'debug'
+set system syslog remote 192.168.0.252 protocol 'udp'
set system task-scheduler task Update-Blacklists executable path '/config/scripts/vyos-foo-update.script'
set system task-scheduler task Update-Blacklists interval '3h'
set system time-zone 'Pacific/Auckland'
diff --git a/smoketest/config-tests/dialup-router-wireguard-ipv6 b/smoketest/config-tests/dialup-router-wireguard-ipv6
index 3e298fb82..269e9d722 100644
--- a/smoketest/config-tests/dialup-router-wireguard-ipv6
+++ b/smoketest/config-tests/dialup-router-wireguard-ipv6
@@ -691,7 +691,7 @@ set system option ctrl-alt-delete 'ignore'
set system option performance 'network-latency'
set system option reboot-on-panic
set system option startup-beep
-set system syslog global facility all level 'debug'
-set system syslog global facility local7 level 'debug'
-set system syslog host 172.16.100.1 facility all level 'warning'
+set system syslog local facility all level 'debug'
+set system syslog local facility local7 level 'debug'
+set system syslog remote 172.16.100.1 facility all level 'warning'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/egp-igp-route-maps b/smoketest/config-tests/egp-igp-route-maps
index fc46d25ff..222325cd7 100644
--- a/smoketest/config-tests/egp-igp-route-maps
+++ b/smoketest/config-tests/egp-igp-route-maps
@@ -42,5 +42,5 @@ set system login user vyos authentication plaintext-password ''
set system logs logrotate messages max-size '1'
set system logs logrotate messages rotate '5'
set system name-server '192.168.0.1'
-set system syslog global facility all level 'info'
+set system syslog local facility all level 'info'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/igmp-pim-small b/smoketest/config-tests/igmp-pim-small
index 909c3d67b..06051af41 100644
--- a/smoketest/config-tests/igmp-pim-small
+++ b/smoketest/config-tests/igmp-pim-small
@@ -32,6 +32,6 @@ set system domain-name 'vyos.io'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/ipoe-server b/smoketest/config-tests/ipoe-server
index f4a12f502..c21495ab2 100644
--- a/smoketest/config-tests/ipoe-server
+++ b/smoketest/config-tests/ipoe-server
@@ -44,5 +44,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/ipv6-disable b/smoketest/config-tests/ipv6-disable
index 40e34fa0c..5f906b5f7 100644
--- a/smoketest/config-tests/ipv6-disable
+++ b/smoketest/config-tests/ipv6-disable
@@ -27,5 +27,5 @@ set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX
set system login user vyos authentication plaintext-password ''
set system name-server '172.16.254.20'
set system name-server '172.16.254.30'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/isis-small b/smoketest/config-tests/isis-small
index b322f4e29..e61d0362e 100644
--- a/smoketest/config-tests/isis-small
+++ b/smoketest/config-tests/isis-small
@@ -39,6 +39,6 @@ set system login user vyos authentication plaintext-password ''
set service ntp server time1.vyos.net
set service ntp server time2.vyos.net
set service ntp server time3.vyos.net
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/nat-basic b/smoketest/config-tests/nat-basic
index 471add3b3..f1cc0121d 100644
--- a/smoketest/config-tests/nat-basic
+++ b/smoketest/config-tests/nat-basic
@@ -60,7 +60,7 @@ set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0
set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 stop '192.168.189.254'
set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 subnet-id '1'
set service lldp interface all
-set service lldp interface eth1 disable
+set service lldp interface eth1 mode 'disable'
set service ntp allow-client address '192.168.189.0/24'
set service ntp listen-address '192.168.189.1'
set service ntp server time1.vyos.net
@@ -84,5 +84,5 @@ set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX
set system login user vyos authentication plaintext-password ''
set system name-server '1.1.1.1'
set system name-server '9.9.9.9'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/ospf-simple b/smoketest/config-tests/ospf-simple
index 355709448..4273e4b8f 100644
--- a/smoketest/config-tests/ospf-simple
+++ b/smoketest/config-tests/ospf-simple
@@ -20,5 +20,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'lab-vyos-r1'
set system login user vyos authentication encrypted-password '$6$R.OnGzfXSfl6J$Iba/hl9bmjBs0VPtZ2zdW.Snh/nHuvxUwi0R6ruypgW63iKEbicJH.uUst8xZCyByURblxRtjAC1lAnYfIt.b0'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/ospf-small b/smoketest/config-tests/ospf-small
index a7f8b682c..af69e5702 100644
--- a/smoketest/config-tests/ospf-small
+++ b/smoketest/config-tests/ospf-small
@@ -77,6 +77,6 @@ set system sysctl parameter net.ipv4.igmp_max_memberships value '5'
set system sysctl parameter net.ipv4.ipfrag_time value '4'
set system sysctl parameter net.mpls.default_ttl value '10'
set system sysctl parameter net.mpls.ip_ttl_propagate value '0'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
diff --git a/smoketest/config-tests/pppoe-server b/smoketest/config-tests/pppoe-server
index 34fbea215..e488fc746 100644
--- a/smoketest/config-tests/pppoe-server
+++ b/smoketest/config-tests/pppoe-server
@@ -43,5 +43,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/qos-basic b/smoketest/config-tests/qos-basic
index 0e198b80c..655a5794e 100644
--- a/smoketest/config-tests/qos-basic
+++ b/smoketest/config-tests/qos-basic
@@ -71,5 +71,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/rip-router b/smoketest/config-tests/rip-router
index 829aafbd5..d22f424a5 100644
--- a/smoketest/config-tests/rip-router
+++ b/smoketest/config-tests/rip-router
@@ -79,5 +79,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/rpki-only b/smoketest/config-tests/rpki-only
index dcbc7673d..f3e2a74b9 100644
--- a/smoketest/config-tests/rpki-only
+++ b/smoketest/config-tests/rpki-only
@@ -38,5 +38,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$r/Yw/07NXNY$/ZB.Rjf9jxEV.BYoDyLdH.kH14rU52pOBtrX.4S34qlPt77chflCHvpTCq9a6huLzwaMR50rEICzA5GoIRZlM0'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'debug'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'debug'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/static-route-basic b/smoketest/config-tests/static-route-basic
index d2d33d043..a6135d2c4 100644
--- a/smoketest/config-tests/static-route-basic
+++ b/smoketest/config-tests/static-route-basic
@@ -32,6 +32,6 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Asia/Macau'
diff --git a/smoketest/config-tests/tunnel-broker b/smoketest/config-tests/tunnel-broker
index ee6301c85..5518c303b 100644
--- a/smoketest/config-tests/tunnel-broker
+++ b/smoketest/config-tests/tunnel-broker
@@ -71,5 +71,5 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
diff --git a/smoketest/config-tests/vpn-openconnect-sstp b/smoketest/config-tests/vpn-openconnect-sstp
index 28d7d5daa..e7969f633 100644
--- a/smoketest/config-tests/vpn-openconnect-sstp
+++ b/smoketest/config-tests/vpn-openconnect-sstp
@@ -16,8 +16,8 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$2Ta6TWHd/U$NmrX0x9kexCimeOcYK1MfhMpITF9ELxHcaBU/znBq.X2ukQOj61fVI2UYP/xBzP4QtiTcdkgs7WOQMHWsRymO/'
set system login user vyos authentication plaintext-password ''
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set vpn openconnect authentication local-users username test password 'test'
set vpn openconnect authentication mode local 'password'
set vpn openconnect network-settings client-ip-settings subnet '192.168.160.0/24'
diff --git a/smoketest/config-tests/vrf-basic b/smoketest/config-tests/vrf-basic
index 1d2874a60..0c4e49c52 100644
--- a/smoketest/config-tests/vrf-basic
+++ b/smoketest/config-tests/vrf-basic
@@ -35,8 +35,8 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
set vrf name green protocols static route 20.0.0.0/8 next-hop 1.1.1.1 interface 'eth1'
set vrf name green protocols static route 20.0.0.0/8 next-hop 1.1.1.1 vrf 'default'
diff --git a/smoketest/config-tests/vrf-bgp-pppoe-underlay b/smoketest/config-tests/vrf-bgp-pppoe-underlay
index bd64c914a..e3c765a9a 100644
--- a/smoketest/config-tests/vrf-bgp-pppoe-underlay
+++ b/smoketest/config-tests/vrf-bgp-pppoe-underlay
@@ -143,8 +143,8 @@ set system host-name 'vyos'
set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0'
set system login user vyos authentication plaintext-password ''
set system name-server '192.168.0.1'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
set vrf bind-to-all
set vrf name vyos-test-01 protocols bgp address-family ipv4-unicast network 100.64.50.0/23
diff --git a/smoketest/config-tests/vrf-ospf b/smoketest/config-tests/vrf-ospf
index fd14615e0..53207d565 100644
--- a/smoketest/config-tests/vrf-ospf
+++ b/smoketest/config-tests/vrf-ospf
@@ -28,8 +28,8 @@ set system console device ttyS0 speed '115200'
set system host-name 'vyos'
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 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system time-zone 'Europe/Berlin'
set vrf name blue protocols ospf area 0 network '172.18.201.0/24'
set vrf name blue protocols ospf interface eth2 authentication md5 key-id 30 md5-key 'vyoskey456'
diff --git a/smoketest/config-tests/wireless-basic b/smoketest/config-tests/wireless-basic
index d9e6c8fac..e424b2b0f 100644
--- a/smoketest/config-tests/wireless-basic
+++ b/smoketest/config-tests/wireless-basic
@@ -20,6 +20,6 @@ set system console device ttyS0 speed '115200'
set system domain-name 'dev.vyos.net'
set system host-name 'WR1'
set system login user vyos authentication encrypted-password '$6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0'
-set system syslog global facility all level 'info'
-set system syslog global facility local7 level 'debug'
+set system syslog local facility all level 'info'
+set system syslog local facility local7 level 'debug'
set system wireless country-code 'es'
diff --git a/smoketest/configs/basic-haproxy b/smoketest/configs/basic-haproxy
new file mode 100644
index 000000000..83fffbac6
--- /dev/null
+++ b/smoketest/configs/basic-haproxy
@@ -0,0 +1,153 @@
+interfaces {
+ dummy dum0 {
+ address "172.18.254.203/32"
+ }
+ ethernet eth0 {
+ duplex "auto"
+ speed "auto"
+ vif 203 {
+ address "172.18.203.10/24"
+ }
+ }
+ ethernet eth1 {
+ duplex "auto"
+ speed "auto"
+ }
+ ethernet eth2 {
+ duplex "auto"
+ speed "auto"
+ }
+}
+load-balancing {
+ reverse-proxy {
+ backend webserver {
+ logging {
+ facility all {
+ level "all"
+ }
+ facility daemon {
+ level "all"
+ }
+ facility user {
+ level "info"
+ }
+ }
+ server web01 {
+ address "192.0.2.1"
+ port "443"
+ }
+ ssl {
+ no-verify
+ }
+ }
+ global-parameters {
+ logging {
+ facility all {
+ level "all"
+ }
+ facility daemon {
+ level "all"
+ }
+ facility user {
+ level "info"
+ }
+ }
+ }
+ service frontend {
+ backend "webserver"
+ logging {
+ facility all {
+ level "all"
+ }
+ facility daemon {
+ level "all"
+ }
+ facility user {
+ level "info"
+ }
+ }
+ port "443"
+ ssl {
+ certificate "dummy"
+ }
+ }
+ }
+}
+pki {
+ certificate dummy {
+ certificate "MIIDsTCCApmgAwIBAgIUegVgO1wIN2v44trXZ+Kb1t48uL0wDQYJKoZIhvcNAQELBQAwVzELMAkGA1UEBhMCR0IxEzARBgNVBAgMClNvbWUtU3RhdGUxEjAQBgNVBAcMCVNvbWUtQ2l0eTENMAsGA1UECgwEVnlPUzEQMA4GA1UEAwwHdnlvcy5pbzAeFw0yNTA1MDUxODIzMTdaFw0yNjA1MDUxODIzMTdaMFcxCzAJBgNVBAYTAkdCMRMwEQYDVQQIDApTb21lLVN0YXRlMRIwEAYDVQQHDAlTb21lLUNpdHkxDTALBgNVBAoMBFZ5T1MxEDAOBgNVBAMMB3Z5b3MuaW8wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAGjdTBzMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMBMGA1UdJQQMMAoGCCsGAQUFBwMBMB0GA1UdDgQWBBSr4OYIWkb8UGuQnEFnjSbmvR+4vDAfBgNVHSMEGDAWgBSr4OYIWkb8UGuQnEFnjSbmvR+4vDANBgkqhkiG9w0BAQsFAAOCAQEAUmRWRPGXsvfuRT+53id3EufH1IJAdowrt6yBZsHobvqCXO2+YhG7oG6/UqUYiv5bHN5xEMQyWd7nyrLOUeFo2bpcMIOlpl6AoUIY65Gm2BqQ7FuPxLLO25RdpZ5WkMGX5kJsKY0/PcpamRKNz1khgFcRyxf9WGhCAIjDCWIWs8lkvPN3m75SFCW7MTuzzQOrzvI6nqqcHO4k8hRBznp26WLUW1rQKpNN09nZGOkeNYK5QbzKN/RUmtEHQZhlgLAIr09jUaA4RDLI1SdD6LR5nvpa9RJBTyS/kISF8BXKMgvUbDHN2nP+VUUrut2ZwoU+pxV4RVT2pS760HuYj4+sYQ=="
+ private {
+ key "MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDEfMAwYLKKVhGlUXr9gkVC7uBi+0O9yyEgd5QzPByePXYw0FrSLWmLRfQuByFDPIVANcEa3FgIXIAeKmxItw7IhFRsG5soSOXXgBxdAH/qzEbWhwzgafnxZKJkmrQr8YA3IFtkFPr2+5s26WdjtwEM0tzIFkq6hmWSX1axUgvYlF2uCxjututMZ6I5JCa0uR3gBRuNONuGPH3Ko9zUEATffv53j9DbYVEM0lfVNewefPoVJmWz+oT0wP/kNx6tREf+aUAF4m+eBsqnggITftW2fyeFnoBPCcPp3HUgSwZhesunqz+YeW6Pk+WWb5vl+2QbMKKtz5qK6dI3q0z9yp4FAgMBAAECgf9plqCMg2pKEWRFl183bqWAm7lnLnsUOfABFNPYa3U+uKQUKZpboTBfzDfZqNak3XNQV0mTAR8pFfoMhQjQU9hUxH7ivjw1RUCHjixCF0vLBkTB34gL7FUbiEIFhR1NW3pCJY73OXOkIZG1Obh6Syb8KubDeu4bTmb90/TnDDAs6OYXJ5yo7ZDZLvLu5a3Dli+H4K5Qb5VJ74o/vtodBo3wmKBgy2Ey6JqF+y7/3HeE66rVhYNft5pURgemWnNYqh3oDTJASqpA/8n90o8ceYPVJugQ7029UiyTp0xgBRXFszgiYPkBlsNWB+9+ospopOmYU0owBykH+RtD/bQ0mwECgYEA4gxPdYbHg/GLihHrkv0t5V0pSEhBeBeTBdWG+P/6K90vXofpp7qISdOeYMGkh8mY+PfZNHksdu67d2ks5on5/dNf5YXWCm+LMMRiSsfOo11NISNdNHS6afqs18Wq1aKawv/rwotfxrakM4Gar692+jgz/l/X+FdOQwmE6uEus8ECgYEA3oW4eGtzGq28TiqyhTHduTkas0ckPWX8ulasyPnLxBKDNNohbXXpFIJIcrnl24QFJw3MJbo+R+OxZHgzPK8r64gIGVa7vLCR2fiU/RFoUa1Jo1pPOqXXWqf/Mvdokm2p0atrjRUX9VhjoFsDLcqTgAmfSCsVSqGucX6ER7Gy60UCgYAvmhoNjNFtFquk6rsqHAjTOTgdUaH/0S8T1nBy9SzQmeaEyKhKuvxCV78NbxnfwnNlUoQ6CZ50eTefINXkwn+TlTSnl/SIBA9SuLheOQ9p1ZcNeG4DQuWStcg6NBUSoghnMg+Ky2Di7slLU2qovpGWhcllMve/A1umwFVuRPdZwQKBgHS7mYYyd/Oq6HnpFDWjbzlXp5Yc3/oFooruJT5ZLHfzbjkvpRGTJW7I2dC1jMuXekx+hHXWOg3keI7IL7jJ/DRW7Ei+o0XdKuY57Y7ErwEJ8vNq0N1nWo4IS2wlNgp61PdVAdrFEgh3EexxUj2XY8FrSs/FKio4nxaS1Dn4EnAxAoGBAKqLvuPpmCMVwlIu57WGxL5d9i7EjIGY65l6HTKQYoHCzE51rkowH1La2fuUYz0IpExq2lcrLbOUtSyhXH7Zlktiz//Gu/P90SUfR/ZGcLeZi+EDyK2OctpnWBjs2Dmfg4D6vxk39yV8AB97pYG073GcJ/P54qRUuEitbpJwH+fB"
+ }
+ }
+}
+service {
+ ntp {
+ allow-client {
+ address "0.0.0.0/0"
+ address "::/0"
+ }
+ server 172.16.100.10 {
+ }
+ server 172.16.100.20 {
+ }
+ server 172.16.110.30 {
+ }
+ }
+ ssh {
+ disable-host-validation
+ port "22"
+ }
+}
+system {
+ config-management {
+ commit-revisions "200"
+ }
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ conntrack {
+ modules {
+ ftp
+ h323
+ nfs
+ pptp
+ sip
+ sqlnet
+ tftp
+ }
+ }
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
+ plaintext-password ""
+ }
+ }
+ }
+ name-server "172.16.254.30"
+ option {
+ kernel {
+ disable-mitigations
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level "info"
+ }
+ facility local7 {
+ level "debug"
+ }
+ }
+ }
+ time-zone "Europe/Berlin"
+}
+
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@8:dhcpv6-server@1:dns-dynamic@4:dns-forwarding@4:firewall@15:flow-accounting@1:https@6:ids@1:interfaces@32:ipoe-server@3:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@1:nat@8:nat66@3:ntp@3:openconnect@3:ospf@2:pim@1:policy@8:pppoe-server@10:pptp@5:qos@2:quagga@11:reverse-proxy@1:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@27:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2"
+// Release version: 1.4.1
diff --git a/smoketest/configs/basic-syslog b/smoketest/configs/basic-syslog
new file mode 100644
index 000000000..9336b73bc
--- /dev/null
+++ b/smoketest/configs/basic-syslog
@@ -0,0 +1,70 @@
+interfaces {
+ ethernet eth0 {
+ duplex "auto"
+ speed "auto"
+ }
+ ethernet eth1 {
+ address 172.16.33.154/24
+ duplex auto
+ speed auto
+ vrf red
+ }
+}
+system {
+ console {
+ device ttyS0 {
+ speed 115200
+ }
+ }
+ domain-name vyos-ci-test.net
+ host-name vyos
+ login {
+ user vyos {
+ authentication {
+ encrypted-password $6$O5gJRlDYQpj$MtrCV9lxMnZPMbcxlU7.FI793MImNHznxGoMFgm3Q6QP3vfKJyOSRCt3Ka/GzFQyW1yZS4NS616NLHaIPPFHc0
+ plaintext-password ""
+ }
+ }
+ }
+ syslog {
+ global {
+ facility all {
+ level info
+ }
+ facility local7 {
+ level debug
+ }
+ marker {
+ interval 999
+ }
+ preserve-fqdn
+ }
+ host syslog01.vyos.net {
+ facility local7 {
+ level notice
+ }
+ port 8000
+ }
+ host syslog02.vyos.net {
+ facility all {
+ level debug
+ }
+ format {
+ include-timezone
+ octet-counted
+ }
+ protocol tcp
+ port 8001
+ }
+ vrf red
+ }
+}
+vrf {
+ name red {
+ table 12321
+ }
+}
+
+// Warning: Do not remove the following line.
+// vyos-config-version: "bgp@5:broadcast-relay@1:cluster@2:config-management@1:conntrack@5:conntrack-sync@2:container@2:dhcp-relay@2:dhcp-server@8:dhcpv6-server@1:dns-dynamic@4:dns-forwarding@4:firewall@15:flow-accounting@1:https@6:ids@1:interfaces@32:ipoe-server@3:ipsec@13:isis@3:l2tp@9:lldp@2:mdns@1:monitoring@1:nat@8:nat66@3:ntp@3:openconnect@3:ospf@2:pim@1:policy@8:pppoe-server@10:pptp@5:qos@2:quagga@11:reverse-proxy@1:rip@1:rpki@2:salt@1:snmp@3:ssh@2:sstp@6:system@27:vrf@3:vrrp@4:vyos-accel-ppp@2:wanloadbalance@3:webproxy@2"
+// Release version: 1.4.0
diff --git a/smoketest/configs/basic-vyos b/smoketest/configs/basic-vyos
index 242f3d1de..5f7a71237 100644
--- a/smoketest/configs/basic-vyos
+++ b/smoketest/configs/basic-vyos
@@ -99,33 +99,77 @@ protocols {
}
service {
dhcp-server {
+ dynamic-dns-update {
+ send-updates enable
+ forward-domain domain.lan {
+ dns-server 1 {
+ address 192.168.0.1
+ }
+ dns-server 2 {
+ address 100.100.0.1
+ }
+ key-name domain-lan-updates
+ }
+ reverse-domain 0.168.192.in-addr.arpa {
+ dns-server 1 {
+ address 192.168.0.1
+ }
+ dns-server 2 {
+ address 100.100.0.1
+ }
+ key-name reverse-0-168-192
+ }
+ tsig-key domain-lan-updates {
+ algorithm sha256
+ secret SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ==
+ }
+ tsig-key reverse-0-168-192 {
+ algorithm sha256
+ secret VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ==
+ }
+ conflict-resolution enable
+ }
shared-network-name LAN {
authoritative
+ dynamic-dns-update {
+ send-updates enable
+ ttl-percent 75
+ }
subnet 192.168.0.0/24 {
- default-router 192.168.0.1
- dns-server 192.168.0.1
- domain-name vyos.net
- domain-search vyos.net
+ dynamic-dns-update {
+ send-updates enable
+ generated-prefix myhost
+ qualifying-suffix lan1.domain.lan
+ }
+ option {
+ default-router 192.168.0.1
+ domain-name vyos.net
+ domain-search vyos.net
+ name-server 192.168.0.1
+ }
range LANDynamic {
start 192.168.0.30
stop 192.168.0.240
}
static-mapping TEST1-1 {
ip-address 192.168.0.11
- mac-address 00:01:02:03:04:05
+ mac 00:01:02:03:04:05
}
static-mapping TEST1-2 {
+ disable
ip-address 192.168.0.12
- mac-address 00:01:02:03:04:05
+ mac 00:01:02:03:04:05
}
static-mapping TEST2-1 {
ip-address 192.168.0.21
- mac-address 00:01:02:03:04:21
+ mac 00:01:02:03:04:21
}
static-mapping TEST2-2 {
+ disable
ip-address 192.168.0.21
- mac-address 00:01:02:03:04:22
+ mac 00:01:02:03:04:22
}
+ subnet-id 1
}
}
}
@@ -236,6 +280,9 @@ system {
facility security {
level info
}
+ marker {
+ interval 1000
+ }
preserve-fqdn
}
host syslog.vyos.net {
@@ -251,6 +298,7 @@ system {
format {
octet-counted
}
+ protocol tcp
port 8000
}
}
diff --git a/smoketest/configs/bgp-rpki b/smoketest/configs/bgp-rpki
index 5588f15c9..2d136d545 100644
--- a/smoketest/configs/bgp-rpki
+++ b/smoketest/configs/bgp-rpki
@@ -46,6 +46,13 @@ policy {
}
protocols {
bgp 64500 {
+ address-family {
+ ipv4-unicast {
+ redistribute {
+ table 100
+ }
+ }
+ }
neighbor 1.2.3.4 {
address-family {
ipv4-unicast {
diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex
index aa9837fe9..018379bcd 100644
--- a/smoketest/configs/dialup-router-complex
+++ b/smoketest/configs/dialup-router-complex
@@ -1392,6 +1392,9 @@ service {
}
}
lldp {
+ interface pppoe0 {
+ disable
+ }
legacy-protocols {
cdp
}
diff --git a/smoketest/scripts/cli/base_interfaces_test.py b/smoketest/scripts/cli/base_interfaces_test.py
index c19bfcfe2..5348b0cc3 100644
--- a/smoketest/scripts/cli/base_interfaces_test.py
+++ b/smoketest/scripts/cli/base_interfaces_test.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright (C) 2019-2025 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,9 +14,11 @@
import re
+from json import loads
from netifaces import AF_INET
from netifaces import AF_INET6
from netifaces import ifaddresses
+from systemd import journal
from base_vyostest_shim import VyOSUnitTestSHIM
from base_vyostest_shim import CSTORE_GUARD_TIME
@@ -38,12 +40,15 @@ from vyos.utils.network import is_intf_addr_assigned
from vyos.utils.network import is_ipv6_link_local
from vyos.utils.network import get_nft_vrf_zone_mapping
from vyos.xml_ref import cli_defined
+from vyos.xml_ref import default_value
dhclient_base_dir = directories['isc_dhclient_dir']
dhclient_process_name = 'dhclient'
dhcp6c_base_dir = directories['dhcp6_client_dir']
dhcp6c_process_name = 'dhcp6c'
+MSG_TESTCASE_UNSUPPORTED = 'unsupported on interface family'
+
server_ca_root_cert_data = """
MIIBcTCCARagAwIBAgIUDcAf1oIQV+6WRaW7NPcSnECQ/lUwCgYIKoZIzj0EAwIw
HjEcMBoGA1UEAwwTVnlPUyBzZXJ2ZXIgcm9vdCBDQTAeFw0yMjAyMTcxOTQxMjBa
@@ -134,6 +139,7 @@ def is_mirrored_to(interface, mirror_if, qdisc):
if mirror_if in tmp:
ret_val = True
return ret_val
+
class BasicInterfaceTest:
class TestCase(VyOSUnitTestSHIM.TestCase):
_test_dhcp = False
@@ -217,7 +223,7 @@ class BasicInterfaceTest:
def test_dhcp_disable_interface(self):
if not self._test_dhcp:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# When interface is configured as admin down, it must be admin down
# even when dhcpc starts on the given interface
@@ -240,7 +246,7 @@ class BasicInterfaceTest:
def test_dhcp_client_options(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
client_id = 'VyOS-router'
distance = '100'
@@ -280,7 +286,10 @@ class BasicInterfaceTest:
def test_dhcp_vrf(self):
if not self._test_dhcp or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
+
+ cli_default_metric = default_value(self._base_path + [self._interfaces[0],
+ 'dhcp-options', 'default-route-distance'])
vrf_name = 'purple4'
self.cli_set(['vrf', 'name', vrf_name, 'table', '65000'])
@@ -307,13 +316,34 @@ class BasicInterfaceTest:
self.assertIn(str(dhclient_pid), vrf_pids)
# and the commandline has the appropriate options
cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
- self.assertIn('-e\x00IF_METRIC=210', cmdline) # 210 is the default value
+ self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline)
+
+ # T5103: remove interface from VRF instance and move DHCP client
+ # back to default VRF. This must restart the DHCP client process
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, 'default')
+ # Check if dhclient process runs
+ 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}')
+ self.assertNotIn(str(dhclient_pid), vrf_pids)
+ # and the commandline has the appropriate options
+ cmdline = read_file(f'/proc/{dhclient_pid}/cmdline')
+ self.assertIn(f'-e\x00IF_METRIC={cli_default_metric}', cmdline)
self.cli_delete(['vrf', 'name', vrf_name])
def test_dhcpv6_vrf(self):
if not self._test_ipv6_dhcpc6 or not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf_name = 'purple6'
self.cli_set(['vrf', 'name', vrf_name, 'table', '65001'])
@@ -341,11 +371,31 @@ class BasicInterfaceTest:
vrf_pids = cmd(f'ip vrf pids {vrf_name}')
self.assertIn(str(tmp), vrf_pids)
+ # T7135: remove interface from VRF instance and move DHCP client
+ # back to default VRF. This must restart the DHCP client process
+ for interface in self._interfaces:
+ self.cli_delete(self._base_path + [interface, 'vrf'])
+
+ self.cli_commit()
+
+ # Validate interface state
+ for interface in self._interfaces:
+ tmp = get_interface_vrf(interface)
+ self.assertEqual(tmp, 'default')
+
+ # Check if dhclient process runs
+ 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}')
+ self.assertNotIn(str(tmp), vrf_pids)
+
+
self.cli_delete(['vrf', 'name', vrf_name])
def test_move_interface_between_vrf_instances(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
vrf1_name = 'smoketest_mgmt1'
vrf1_table = '5424'
@@ -390,7 +440,7 @@ class BasicInterfaceTest:
def test_add_to_invalid_vrf(self):
if not self._test_vrf:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# move interface into first VRF
for interface in self._interfaces:
@@ -408,7 +458,7 @@ class BasicInterfaceTest:
def test_span_mirror(self):
if not self._mirror_interfaces:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
# Check the two-way mirror rules of ingress and egress
for mirror in self._mirror_interfaces:
@@ -517,7 +567,7 @@ class BasicInterfaceTest:
def test_ipv6_link_local_address(self):
# Common function for IPv6 link-local address assignemnts
if not self._test_ipv6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -548,7 +598,7 @@ class BasicInterfaceTest:
def test_interface_mtu(self):
if not self._test_mtu:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for intf in self._interfaces:
base = self._base_path + [intf]
@@ -567,8 +617,8 @@ class BasicInterfaceTest:
def test_mtu_1200_no_ipv6_interface(self):
# Testcase if MTU can be changed to 1200 on non IPv6
# enabled interfaces
- if not self._test_mtu:
- self.skipTest('not supported')
+ if not self._test_mtu or not self._test_ipv6:
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
old_mtu = self._mtu
self._mtu = '1200'
@@ -604,7 +654,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -649,7 +699,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan or not self._test_mtu:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
mtu_1500 = '1500'
mtu_9000 = '9000'
@@ -695,7 +745,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -765,7 +815,7 @@ class BasicInterfaceTest:
def test_vif_8021q_lower_up_down(self):
# Testcase for https://vyos.dev/T3349
if not self._test_vlan:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -805,7 +855,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_qinq:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -872,7 +922,7 @@ class BasicInterfaceTest:
# which creates a wlan0 and wlan1 interface which will fail the
# tearDown() test in the end that no interface is allowed to survive!
if not self._test_qinq:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
for interface in self._interfaces:
base = self._base_path + [interface]
@@ -910,7 +960,7 @@ class BasicInterfaceTest:
def test_interface_ip_options(self):
if not self._test_ip:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
arp_tmo = '300'
mss = '1420'
@@ -1012,12 +1062,13 @@ class BasicInterfaceTest:
def test_interface_ipv6_options(self):
if not self._test_ipv6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
mss = '1400'
dad_transmits = '10'
accept_dad = '0'
source_validation = 'strict'
+ interface_identifier = '::fffe'
for interface in self._interfaces:
path = self._base_path + [interface]
@@ -1040,6 +1091,9 @@ class BasicInterfaceTest:
if cli_defined(self._base_path + ['ipv6'], 'source-validation'):
self.cli_set(path + ['ipv6', 'source-validation', source_validation])
+ if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'):
+ self.cli_set(path + ['ipv6', 'address', 'interface-identifier', interface_identifier])
+
self.cli_commit()
for interface in self._interfaces:
@@ -1071,13 +1125,20 @@ class BasicInterfaceTest:
self.assertIn('fib saddr . iif oif 0', line)
self.assertIn('drop', line)
+ if cli_defined(self._base_path + ['ipv6', 'address'], 'interface-identifier'):
+ tmp = cmd(f'ip -j token show dev {interface}')
+ tmp = loads(tmp)[0]
+ self.assertEqual(tmp['token'], interface_identifier)
+ self.assertEqual(tmp['ifname'], interface)
+
+
def test_dhcpv6_client_options(self):
if not self._test_ipv6_dhcpc6:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
duid_base = 10
for interface in self._interfaces:
- duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}'
path = self._base_path + [interface]
for option in self._options.get(interface, []):
self.cli_set(path + option.split())
@@ -1094,7 +1155,7 @@ class BasicInterfaceTest:
duid_base = 10
for interface in self._interfaces:
- duid = '00:01:00:01:27:71:db:f0:00:50:00:00:00:{}'.format(duid_base)
+ duid = f'00:01:00:01:27:71:db:f0:00:50:00:00:00:{duid_base}'
dhcpc6_config = read_file(f'{dhcp6c_base_dir}/dhcp6c.{interface}.conf')
self.assertIn(f'interface {interface} ' + '{', dhcpc6_config)
self.assertIn(f' request domain-name-servers;', dhcpc6_config)
@@ -1106,16 +1167,25 @@ class BasicInterfaceTest:
self.assertIn('};', dhcpc6_config)
duid_base += 1
+ # T7058: verify daemon has no problems understanding the custom DUID option
+ j = journal.Reader()
+ j.this_boot()
+ j.add_match(_SYSTEMD_UNIT=f'dhcp6c@{interface}.service')
+ for entry in j:
+ self.assertNotIn('yyerror0', entry.get('MESSAGE', ''))
+ self.assertNotIn('syntax error', entry.get('MESSAGE', ''))
+
# Better ask the process about it's commandline in the future
pid = process_named_running(dhcp6c_process_name, cmdline=interface, timeout=10)
self.assertTrue(pid)
+ # DHCPv6 option "no-release" requires "-n" daemon startup option
dhcp6c_options = read_file(f'/proc/{pid}/cmdline')
self.assertIn('-n', dhcp6c_options)
def test_dhcpv6pd_auto_sla_id(self):
if not self._test_ipv6_pd:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
@@ -1176,7 +1246,7 @@ class BasicInterfaceTest:
def test_dhcpv6pd_manual_sla_id(self):
if not self._test_ipv6_pd:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
prefix_len = '56'
sla_len = str(64 - int(prefix_len))
@@ -1242,7 +1312,7 @@ class BasicInterfaceTest:
def test_eapol(self):
if not self._test_eapol:
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
cfg_dir = '/run/wpa_supplicant'
diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py
index a54622700..f0674f187 100644
--- a/smoketest/scripts/cli/base_vyostest_shim.py
+++ b/smoketest/scripts/cli/base_vyostest_shim.py
@@ -75,10 +75,11 @@ class VyOSUnitTestSHIM:
cls._session.discard()
cls.fail(cls)
- def cli_set(self, config):
+ def cli_set(self, path, value=None):
if self.debug:
- print('set ' + ' '.join(config))
- self._session.set(config)
+ str = f'set {" ".join(path)} {value}' if value else f'set {" ".join(path)}'
+ print(str)
+ self._session.set(path, value)
def cli_delete(self, config):
if self.debug:
@@ -93,14 +94,18 @@ class VyOSUnitTestSHIM:
def cli_commit(self):
if self.debug:
print('commit')
- self._session.commit()
# During a commit there is a process opening commit_lock, and run()
# returns 0
while run(f'sudo lsof -nP {commit_lock}') == 0:
sleep(0.250)
+ # Return the output of commit
+ # Necessary for testing Warning cases
+ out = self._session.commit()
# Wait for CStore completion for fast non-interactive commits
sleep(self._commit_guard_time)
+ return out
+
def op_mode(self, path : list) -> None:
"""
Execute OP-mode command and return stdout
@@ -182,6 +187,15 @@ class VyOSUnitTestSHIM:
break
self.assertTrue(not matched if inverse else matched, msg=search)
+ def verify_nftables_chain_exists(self, table, chain, inverse=False):
+ try:
+ cmd(f'sudo nft list chain {table} {chain}')
+ if inverse:
+ self.fail(f'Chain exists: {table} {chain}')
+ except OSError:
+ if not inverse:
+ self.fail(f'Chain does not exist: {table} {chain}')
+
# Verify ip rule output
def verify_rules(self, rules_search, inverse=False, addr_family='inet'):
rule_output = cmd(f'ip -family {addr_family} rule show')
diff --git a/smoketest/scripts/cli/test_container.py b/smoketest/scripts/cli/test_container.py
index 36622cad1..daad3a909 100755
--- a/smoketest/scripts/cli/test_container.py
+++ b/smoketest/scripts/cli/test_container.py
@@ -33,11 +33,13 @@ PROCESS_PIDFILE = '/run/vyos-container-{0}.service.pid'
busybox_image = 'busybox:stable'
busybox_image_path = '/usr/share/vyos/busybox-stable.tar'
+
def cmd_to_json(command):
c = cmd(command + ' --format=json')
data = json.loads(c)[0]
return data
+
class TestContainer(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
@@ -73,13 +75,26 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
cont_name = 'c1'
self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', '10.0.2.15/24'])
- self.cli_set(['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2'])
+ self.cli_set(
+ ['protocols', 'static', 'route', '0.0.0.0/0', 'next-hop', '10.0.2.2']
+ )
self.cli_set(['system', 'name-server', '1.1.1.1'])
self.cli_set(['system', 'name-server', '8.8.8.8'])
self.cli_set(base_path + ['name', cont_name, 'image', busybox_image])
self.cli_set(base_path + ['name', cont_name, 'allow-host-networks'])
- self.cli_set(base_path + ['name', cont_name, 'sysctl', 'parameter', 'kernel.msgmax', 'value', '4096'])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ cont_name,
+ 'sysctl',
+ 'parameter',
+ 'kernel.msgmax',
+ 'value',
+ '4096',
+ ]
+ )
# commit changes
self.cli_commit()
@@ -95,6 +110,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
tmp = cmd(f'sudo podman exec -it {cont_name} sysctl kernel.msgmax')
self.assertEqual(tmp, 'kernel.msgmax = 4096')
+ def test_log_driver(self):
+ self.cli_set(base_path + ['log-driver', 'journald'])
+
+ self.cli_commit()
+
+ tmp = cmd('podman info --format "{{ .Host.LogDriver }}"')
+ self.assertEqual(tmp, 'journald')
+
def test_name_server(self):
cont_name = 'dns-test'
net_name = 'net-test'
@@ -105,7 +128,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['name', cont_name, 'image', busybox_image])
self.cli_set(base_path + ['name', cont_name, 'name-server', name_server])
- self.cli_set(base_path + ['name', cont_name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ cont_name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + 2),
+ ]
+ )
# verify() - name server has no effect when container network has dns enabled
with self.assertRaises(ConfigSessionError):
@@ -146,7 +179,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
self.cli_set(base_path + ['name', name, 'image', busybox_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -163,8 +206,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(2, 6):
name = f'{base_name}-{ii}'
c = cmd_to_json(f'sudo podman container inspect {name}')
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['Gateway'] , str(ip_interface(prefix).ip + 1))
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPAddress'], str(ip_interface(prefix).ip + ii))
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['Gateway'],
+ str(ip_interface(prefix).ip + 1),
+ )
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['IPAddress'],
+ str(ip_interface(prefix).ip + ii),
+ )
def test_ipv6_network(self):
prefix = '2001:db8::/64'
@@ -176,7 +225,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
self.cli_set(base_path + ['name', name, 'image', busybox_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + ii)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -193,8 +252,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(2, 6):
name = f'{base_name}-{ii}'
c = cmd_to_json(f'sudo podman container inspect {name}')
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix).ip + 1))
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix).ip + ii))
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'],
+ str(ip_interface(prefix).ip + 1),
+ )
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'],
+ str(ip_interface(prefix).ip + ii),
+ )
def test_dual_stack_network(self):
prefix4 = '192.0.2.0/24'
@@ -208,8 +273,28 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(1, 6):
name = f'{base_name}-{ii}'
self.cli_set(base_path + ['name', name, 'image', busybox_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix4).ip + ii)])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix6).ip + ii)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix4).ip + ii),
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix6).ip + ii),
+ ]
+ )
# verify() - first IP address of a prefix can not be used by a container
with self.assertRaises(ConfigSessionError):
@@ -227,10 +312,22 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
for ii in range(2, 6):
name = f'{base_name}-{ii}'
c = cmd_to_json(f'sudo podman container inspect {name}')
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'] , str(ip_interface(prefix6).ip + 1))
- self.assertEqual(c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'], str(ip_interface(prefix6).ip + ii))
- 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))
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['IPv6Gateway'],
+ str(ip_interface(prefix6).ip + 1),
+ )
+ self.assertEqual(
+ c['NetworkSettings']['Networks'][net_name]['GlobalIPv6Address'],
+ str(ip_interface(prefix6).ip + ii),
+ )
+ 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_no_name_server(self):
prefix = '192.0.2.0/24'
@@ -242,7 +339,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
name = f'{base_name}-2'
self.cli_set(base_path + ['name', name, 'image', busybox_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + 2),
+ ]
+ )
self.cli_commit()
n = cmd_to_json(f'sudo podman network inspect {net_name}')
@@ -258,7 +365,17 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
name = f'{base_name}-2'
self.cli_set(base_path + ['name', name, 'image', busybox_image])
- self.cli_set(base_path + ['name', name, 'network', net_name, 'address', str(ip_interface(prefix).ip + 2)])
+ self.cli_set(
+ base_path
+ + [
+ 'name',
+ name,
+ 'network',
+ net_name,
+ 'address',
+ str(ip_interface(prefix).ip + 2),
+ ]
+ )
self.cli_commit()
n = cmd_to_json(f'sudo podman network inspect {net_name}')
@@ -298,11 +415,14 @@ class TestContainer(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
# Query API about running containers
- tmp = cmd("sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json")
+ tmp = cmd(
+ "sudo curl --unix-socket /run/podman/podman.sock -H 'content-type: application/json' -sf http://localhost/containers/json"
+ )
tmp = json.loads(tmp)
# We expect the same amount of containers from the API that we started above
self.assertEqual(len(container_list), len(tmp))
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py
index 10301831e..851a15f16 100755
--- a/smoketest/scripts/cli/test_firewall.py
+++ b/smoketest/scripts/cli/test_firewall.py
@@ -119,6 +119,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com'])
self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org'])
self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest'])
self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept'])
@@ -133,6 +134,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface'])
+ # Create container network so test won't fail
+ self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24'])
+
self.cli_commit()
self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5')
@@ -638,6 +642,10 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip6 vyos_filter')
def test_ipv4_global_state(self):
+ self.cli_set(['firewall', 'flowtable', 'smoketest', 'interface', 'eth0'])
+ self.cli_set(['firewall', 'flowtable', 'smoketest', 'offload', 'software'])
+
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'offload', 'offload-target', 'smoketest'])
self.cli_set(['firewall', 'global-options', 'state-policy', 'established', 'action', 'accept'])
self.cli_set(['firewall', 'global-options', 'state-policy', 'related', 'action', 'accept'])
self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'drop'])
@@ -647,6 +655,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
nftables_search = [
['jump VYOS_STATE_POLICY'],
['chain VYOS_STATE_POLICY'],
+ ['jump VYOS_STATE_POLICY_FORWARD'],
+ ['chain VYOS_STATE_POLICY_FORWARD'],
+ ['flow add @VYOS_FLOWTABLE_smoketest'],
['ct state established', 'accept'],
['ct state invalid', 'drop'],
['ct state related', 'accept']
@@ -654,6 +665,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip vyos_filter')
+ # T7148 - Ensure bridge rule reject -> drop
+ self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject'])
+ self.cli_commit()
+
+ self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter')
+ self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter')
+
# Check conntrack is enabled from state-policy
self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK')
self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK')
@@ -1167,7 +1185,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4'])
-
+
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4'])
self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump'])
@@ -1202,8 +1220,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump'])
self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1'])
- # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
- # It should be caught by conf validation.
+ # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not.
+ # It should be caught by conf validation.
with self.assertRaises(ConfigSessionError):
self.cli_commit()
@@ -1262,5 +1280,113 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError):
self.cli_commit()
+ def test_ipv4_remote_group(self):
+ # Setup base config for test
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ 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', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft
+ nftables_search = [
+ ['R_group01'],
+ ['type ipv4_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R_group01', 'ipv4-INP-filter-10']
+ ]
+ self.verify_nftables(nftables_search, 'ip vyos_filter')
+
+ # Test remote-group cannot be configured without a URL
+ self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+ # Test remote-group cannot be set alongside address in rules
+ self.cli_set(['firewall', 'ipv4', 'input', 'filter', 'rule', '10', 'destination', 'address', '127.0.0.1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+
+ def test_ipv6_remote_group(self):
+ # Setup base config for test
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ 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', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft
+ nftables_search = [
+ ['R6_group01'],
+ ['type ipv6_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R6_group01', 'ipv6-INP-filter-10']
+ ]
+ self.verify_nftables(nftables_search, 'ip6 vyos_filter')
+
+ # Test remote-group cannot be configured without a URL
+ self.cli_delete(['firewall', 'group', 'remote-group', 'group01', 'url'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+ # Test remote-group cannot be set alongside address in rules
+ self.cli_set(['firewall', 'ipv6', 'input', 'filter', 'rule', '10', 'destination', 'address', '2001:db8::1'])
+
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_discard()
+
+
+ def test_remote_group(self):
+ # Setup base config for test adding remote group to both ipv4 and ipv6 rules
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'url', 'http://127.0.0.1:80/list.txt'])
+ self.cli_set(['firewall', 'group', 'remote-group', 'group01', 'description', 'Example Group 01'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+ 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', 'source', 'group', 'remote-group', 'group01'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'action', 'drop'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'protocol', 'tcp'])
+ self.cli_set(['firewall', 'ipv6', 'output', 'filter', 'rule', '10', 'destination', 'group', 'remote-group', 'group01'])
+ 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', 'source', 'group', 'remote-group', 'group01'])
+
+ self.cli_commit()
+
+ # Test remote-group had been loaded correctly in nft ip table
+ nftables_v4_search = [
+ ['R_group01'],
+ ['type ipv4_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R_group01', 'ipv4-OUT-filter-10'],
+ ['meta l4proto', 'saddr @R_group01', 'ipv4-INP-filter-10'],
+ ]
+ self.verify_nftables(nftables_v4_search, 'ip vyos_filter')
+
+ # Test remote-group had been loaded correctly in nft ip6 table
+ nftables_v6_search = [
+ ['R6_group01'],
+ ['type ipv6_addr'],
+ ['flags interval'],
+ ['meta l4proto', 'daddr @R6_group01', 'ipv6-OUT-filter-10'],
+ ['meta l4proto', 'saddr @R6_group01', 'ipv6-INP-filter-10'],
+ ]
+ self.verify_nftables(nftables_v6_search, 'ip6 vyos_filter')
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py
index 1a72f9dc4..f99fd0363 100755
--- a/smoketest/scripts/cli/test_interfaces_bonding.py
+++ b/smoketest/scripts/cli/test_interfaces_bonding.py
@@ -167,18 +167,25 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase):
def test_bonding_multi_use_member(self):
# Define available bonding hash policies
- for interface in ['bond10', 'bond20']:
+ bonds = ['bond10', 'bond20', 'bond30']
+ for interface in bonds:
for member in self._members:
self.cli_set(self._base_path + [interface, 'member', 'interface', member])
# check validate() - can not use the same member interfaces multiple times
with self.assertRaises(ConfigSessionError):
self.cli_commit()
-
- self.cli_delete(self._base_path + ['bond20'])
+ # only keep the first bond interface configuration
+ for interface in bonds[1:]:
+ self.cli_delete(self._base_path + [interface])
self.cli_commit()
+ bond = bonds[0]
+ member_ifaces = read_file(f'/sys/class/net/{bond}/bonding/slaves').split()
+ for member in self._members:
+ self.assertIn(member, member_ifaces)
+
def test_bonding_source_interface(self):
# Re-use member interface that is already a source-interface
bond = 'bond99'
diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py
index 54c981adc..4041b3ef3 100755
--- a/smoketest/scripts/cli/test_interfaces_bridge.py
+++ b/smoketest/scripts/cli/test_interfaces_bridge.py
@@ -158,6 +158,21 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase):
# verify member is assigned to the bridge
self.assertEqual(interface, tmp['master'])
+ def test_bridge_multi_use_member(self):
+ # Define available bonding hash policies
+ bridges = ['br10', 'br20', 'br30']
+ for interface in bridges:
+ for member in self._members:
+ self.cli_set(self._base_path + [interface, 'member', 'interface', member])
+
+ # check validate() - can not use the same member interfaces multiple times
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ # only keep the first bond interface configuration
+ for interface in bridges[1:]:
+ self.cli_delete(self._base_path + [interface])
+
+ self.cli_commit()
def test_add_remove_bridge_member(self):
# Add member interfaces to bridge and set STP cost/priority
diff --git a/smoketest/scripts/cli/test_interfaces_loopback.py b/smoketest/scripts/cli/test_interfaces_loopback.py
index 0454dc658..f4b6038c5 100755
--- a/smoketest/scripts/cli/test_interfaces_loopback.py
+++ b/smoketest/scripts/cli/test_interfaces_loopback.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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_interfaces_test import BasicInterfaceTest
+from base_interfaces_test import MSG_TESTCASE_UNSUPPORTED
from netifaces import interfaces
from vyos.utils.network import is_intf_addr_assigned
@@ -53,7 +54,7 @@ class LoopbackInterfaceTest(BasicInterfaceTest.TestCase):
self.assertTrue(is_intf_addr_assigned('lo', addr))
def test_interface_disable(self):
- self.skipTest('not supported')
+ self.skipTest(MSG_TESTCASE_UNSUPPORTED)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
index c6a4613a7..b2af86139 100755
--- a/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
+++ b/smoketest/scripts/cli/test_interfaces_virtual-ethernet.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 VyOS maintainers and contributors
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 2 or later as
@@ -34,9 +34,6 @@ class VEthInterfaceTest(BasicInterfaceTest.TestCase):
# call base-classes classmethod
super(VEthInterfaceTest, cls).setUpClass()
- def test_vif_8021q_mtu_limits(self):
- self.skipTest('not supported')
-
# As we always need a pair of veth interfaces, we can not rely on the base
# class check to determine if there is a dhcp6c or dhclient instance running.
# This test will always fail as there is an instance running on the peer
diff --git a/smoketest/scripts/cli/test_interfaces_vxlan.py b/smoketest/scripts/cli/test_interfaces_vxlan.py
index b2076b43b..132496124 100755
--- a/smoketest/scripts/cli/test_interfaces_vxlan.py
+++ b/smoketest/scripts/cli/test_interfaces_vxlan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2023 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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
@@ -114,6 +114,30 @@ class VXLANInterfaceTest(BasicInterfaceTest.TestCase):
self.assertEqual(Interface(interface).get_admin_state(), 'up')
ttl += 10
+
+ def test_vxlan_group_remote_error(self):
+ intf = 'vxlan60'
+ options = [
+ 'group 239.4.4.5',
+ 'mtu 1420',
+ 'remote 192.168.0.254',
+ 'source-address 192.168.0.1',
+ 'source-interface eth0',
+ 'vni 60'
+ ]
+ for option in options:
+ opts = option.split()
+ self.cli_set(self._base_path + [intf] + opts)
+
+ # verify() - Both group and remote cannot be specified
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+
+ # Remove blocking CLI option
+ self.cli_delete(self._base_path + [intf, 'group'])
+ self.cli_commit()
+
+
def test_vxlan_external(self):
interface = 'vxlan0'
source_address = '192.0.2.1'
diff --git a/smoketest/scripts/cli/test_interfaces_wireless.py b/smoketest/scripts/cli/test_interfaces_wireless.py
index b8b18f30f..1c69c1be5 100755
--- a/smoketest/scripts/cli/test_interfaces_wireless.py
+++ b/smoketest/scripts/cli/test_interfaces_wireless.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2024 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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
@@ -64,13 +64,23 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
# call base-classes classmethod
super(WirelessInterfaceTest, cls).setUpClass()
- # T5245 - currently testcases are disabled
- cls._test_ipv6 = False
- cls._test_vlan = False
+ # If any wireless interface is based on mac80211_hwsim, disable all
+ # VLAN related testcases. See T5245, T7325
+ tmp = read_file('/proc/modules')
+ if 'mac80211_hwsim' in tmp:
+ cls._test_ipv6 = False
+ cls._test_vlan = False
+ cls._test_qinq = False
+
+ # Loading mac80211_hwsim module created two WIFI Interfaces in the
+ # background (wlan0 and wlan1), remove them to have a clean test start.
+ # This must happen AFTER the above check for unsupported drivers
+ for interface in cls._interfaces:
+ if interface_exists(interface):
+ call(f'sudo iw dev {interface} del')
cls.cli_set(cls, wifi_cc_path + [country])
-
def test_wireless_add_single_ip_address(self):
# derived method to check if member interfaces are enslaved properly
super().test_add_single_ip_address()
@@ -627,9 +637,4 @@ class WirelessInterfaceTest(BasicInterfaceTest.TestCase):
if __name__ == '__main__':
check_kmod('mac80211_hwsim')
- # loading the module created two WIFI Interfaces in the background (wlan0 and wlan1)
- # remove them to have a clean test start
- for interface in ['wlan0', 'wlan1']:
- if interface_exists(interface):
- call(f'sudo iw dev {interface} del')
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py
index 9f412aa95..833e0a92b 100755
--- a/smoketest/scripts/cli/test_load-balancing_haproxy.py
+++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py
@@ -14,11 +14,14 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import re
+import textwrap
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.template import get_default_port
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
@@ -131,7 +134,25 @@ ZXLrtgVJR9W020qTurO2f91qfU8646n11hR9ObBB1IYbagOU0Pw1Nrq/FRp/u2tx
7i7xFz2WEiQeSCPaKYOiqM3t
"""
+haproxy_service_name = 'https_front'
+haproxy_backend_name = 'bk-01'
+def parse_haproxy_config() -> dict:
+ config_str = read_file(HAPROXY_CONF)
+ section_pattern = re.compile(r'^(global|defaults|frontend\s+\S+|backend\s+\S+)', re.MULTILINE)
+ sections = {}
+
+ matches = list(section_pattern.finditer(config_str))
+
+ for i, match in enumerate(matches):
+ section_name = match.group(1).strip()
+ start = match.end()
+ end = matches[i + 1].start() if i + 1 < len(matches) else len(config_str)
+ section_body = config_str[start:end]
+ dedented_body = textwrap.dedent(section_body).strip()
+ sections[section_name] = dedented_body
+
+ return sections
class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
# Check for running process
@@ -146,14 +167,14 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertFalse(process_named_running(PROCESS_NAME))
def base_config(self):
- self.cli_set(base_path + ['service', 'https_front', 'mode', 'http'])
- self.cli_set(base_path + ['service', 'https_front', 'port', '4433'])
- self.cli_set(base_path + ['service', 'https_front', 'backend', 'bk-01'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'http'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'port', '4433'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'backend', haproxy_backend_name])
- self.cli_set(base_path + ['backend', 'bk-01', 'mode', 'http'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'address', '192.0.2.11'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'port', '9090'])
- self.cli_set(base_path + ['backend', 'bk-01', 'server', 'bk-01', 'send-proxy'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'mode', 'http'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'address', '192.0.2.11'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'port', '9090'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'server', haproxy_backend_name, 'send-proxy'])
self.cli_set(base_path + ['global-parameters', 'max-connections', '1000'])
@@ -167,15 +188,15 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.cli_set(['pki', 'certificate', 'smoketest', 'certificate', valid_cert.replace('\n','')])
self.cli_set(['pki', 'certificate', 'smoketest', 'private', 'key', valid_cert_private_key.replace('\n','')])
- def test_01_lb_reverse_proxy_domain(self):
+ def test_reverse_proxy_domain(self):
domains_bk_first = ['n1.example.com', 'n2.example.com', 'n3.example.com']
domain_bk_second = 'n5.example.com'
- frontend = 'https_front'
+ frontend = 'vyos_smoketest'
front_port = '4433'
bk_server_first = '192.0.2.11'
bk_server_second = '192.0.2.12'
- bk_first_name = 'bk-01'
- bk_second_name = 'bk-02'
+ bk_first_name = 'vyosbk-01'
+ bk_second_name = 'vyosbk-02'
bk_server_port = '9090'
mode = 'http'
rule_ten = '10'
@@ -241,9 +262,9 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port}', config)
self.assertIn(f'server {bk_second_name} {bk_server_second}:{bk_server_port} backup', config)
- def test_02_lb_reverse_proxy_cert_not_exists(self):
+ def test_reverse_proxy_cert_not_exists(self):
self.base_config()
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
@@ -253,19 +274,19 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.configure_pki()
self.base_config()
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
# self.assertIn('\nCertificate "cert" does not exist\n', str(e.exception))
- self.cli_delete(base_path + ['service', 'https_front', 'ssl', 'certificate', 'cert'])
- self.cli_set(base_path + ['service', 'https_front', 'ssl', 'certificate', 'smoketest'])
+ self.cli_delete(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'cert'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'ssl', 'certificate', 'smoketest'])
self.cli_commit()
- def test_03_lb_reverse_proxy_ca_not_exists(self):
+ def test_reverse_proxy_ca_not_exists(self):
self.base_config()
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
@@ -275,40 +296,40 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.configure_pki()
self.base_config()
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
# self.assertIn('\nCA certificate "ca-test" does not exist\n', str(e.exception))
- self.cli_delete(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'ca-test'])
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ self.cli_delete(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'ca-test'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest'])
self.cli_commit()
- def test_04_lb_reverse_proxy_backend_ssl_no_verify(self):
+ def test_reverse_proxy_backend_ssl_no_verify(self):
# Setup base
self.configure_pki()
self.base_config()
# Set no-verify option
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'no-verify'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'no-verify'])
self.cli_commit()
# Test no-verify option
config = read_file(HAPROXY_CONF)
- self.assertIn('server bk-01 192.0.2.11:9090 send-proxy ssl verify none', config)
+ self.assertIn(f'server {haproxy_backend_name} 192.0.2.11:9090 send-proxy ssl verify none', config)
# Test setting ca-certificate alongside no-verify option fails, to test config validation
- self.cli_set(base_path + ['backend', 'bk-01', 'ssl', 'ca-certificate', 'smoketest'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'ssl', 'ca-certificate', 'smoketest'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_05_lb_reverse_proxy_backend_http_check(self):
+ def test_reverse_proxy_backend_http_check(self):
# Setup base
self.base_config()
# Set http-check
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'method', 'get'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'method', 'get'])
self.cli_commit()
# Test http-check
@@ -317,8 +338,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check send meth GET', config)
# Set http-check with uri and status
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'uri', '/health'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'uri', '/health'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200'])
self.cli_commit()
# Test http-check with uri and status
@@ -328,8 +349,8 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check expect status 200', config)
# Set http-check with string
- self.cli_delete(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'status', '200'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-check', 'expect', 'string', 'success'])
+ self.cli_delete(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'status', '200'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-check', 'expect', 'string', 'success'])
self.cli_commit()
# Test http-check with string
@@ -339,11 +360,11 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('http-check expect string success', config)
# Test configuring both http-check & health-check fails validation script
- self.cli_set(base_path + ['backend', 'bk-01', 'health-check', 'ldap'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'health-check', 'ldap'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_06_lb_reverse_proxy_tcp_mode(self):
+ def test_reverse_proxy_tcp_mode(self):
frontend = 'tcp_8443'
mode = 'tcp'
front_port = '8433'
@@ -390,27 +411,27 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'mode {mode}', config)
self.assertIn(f'server {bk_name} {bk_server}:{bk_server_port}', config)
- def test_07_lb_reverse_proxy_http_response_headers(self):
+ def test_reverse_proxy_http_response_headers(self):
# Setup base
self.configure_pki()
self.base_config()
# Set example headers in both frontend and backend
- self.cli_set(base_path + ['service', 'https_front', 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
- self.cli_set(base_path + ['backend', 'bk-01', 'http-response-headers', 'Proxy-Backend-ID', 'value', 'bk-01'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'http-response-headers', 'Cache-Control', 'value', 'max-age=604800'])
+ self.cli_set(base_path + ['backend', haproxy_backend_name, 'http-response-headers', 'Proxy-Backend-ID', 'value', haproxy_backend_name])
self.cli_commit()
# Test headers are present in generated configuration file
config = read_file(HAPROXY_CONF)
self.assertIn('http-response set-header Cache-Control \'max-age=604800\'', config)
- self.assertIn('http-response set-header Proxy-Backend-ID \'bk-01\'', config)
+ self.assertIn(f'http-response set-header Proxy-Backend-ID \'{haproxy_backend_name}\'', config)
# Test setting alongside modes other than http is blocked by validation conditions
- self.cli_set(base_path + ['service', 'https_front', 'mode', 'tcp'])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'mode', 'tcp'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
- def test_08_lb_reverse_proxy_tcp_health_checks(self):
+ def test_reverse_proxy_tcp_health_checks(self):
# Setup PKI
self.configure_pki()
@@ -458,7 +479,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
config = read_file(HAPROXY_CONF)
self.assertIn(f'option smtpchk', config)
- def test_09_lb_reverse_proxy_logging(self):
+ def test_reverse_proxy_logging(self):
# Setup base
self.base_config()
self.cli_commit()
@@ -477,7 +498,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local2 warning', config)
# Test backend logging options
- backend_path = base_path + ['backend', 'bk-01']
+ backend_path = base_path + ['backend', haproxy_backend_name]
self.cli_set(backend_path + ['logging', 'facility', 'local3', 'level', 'debug'])
self.cli_set(backend_path + ['logging', 'facility', 'local4', 'level', 'info'])
self.cli_commit()
@@ -488,7 +509,7 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local4 info', config)
# Test service logging options
- service_path = base_path + ['service', 'https_front']
+ service_path = base_path + ['service', haproxy_service_name]
self.cli_set(service_path + ['logging', 'facility', 'local5', 'level', 'notice'])
self.cli_set(service_path + ['logging', 'facility', 'local6', 'level', 'crit'])
self.cli_commit()
@@ -498,16 +519,17 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('log /dev/log local5 notice', config)
self.assertIn('log /dev/log local6 crit', config)
- def test_10_lb_reverse_proxy_http_compression(self):
+ def test_reverse_proxy_http_compression(self):
# Setup base
self.configure_pki()
self.base_config()
# Configure compression in frontend
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'algorithm', 'gzip'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/html'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/javascript'])
- self.cli_set(base_path + ['service', 'https_front', 'http-compression', 'mime-type', 'text/plain'])
+ http_comp_path = base_path + ['service', haproxy_service_name, 'http-compression']
+ self.cli_set(http_comp_path + ['algorithm', 'gzip'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/html'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/javascript'])
+ self.cli_set(http_comp_path + ['mime-type', 'text/plain'])
self.cli_commit()
# Test compression is present in generated configuration file
@@ -517,9 +539,77 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase):
self.assertIn('compression type text/html text/javascript text/plain', config)
# Test setting compression without specifying any mime-types fails verification
- self.cli_delete(base_path + ['service', 'https_front', 'http-compression', 'mime-type'])
+ self.cli_delete(base_path + ['service', haproxy_service_name, 'http-compression', 'mime-type'])
with self.assertRaises(ConfigSessionError) as e:
self.cli_commit()
+ def test_reverse_proxy_timeout(self):
+ t_default_check = '5'
+ t_default_client = '50'
+ t_default_connect = '10'
+ t_default_server ='50'
+ t_check = '4'
+ t_client = '300'
+ t_connect = '12'
+ t_server ='120'
+ t_front_client = '600'
+
+ self.base_config()
+ self.cli_commit()
+ # Check default timeout options
+ config_entries = (
+ f'timeout check {t_default_check}s',
+ f'timeout connect {t_default_connect}s',
+ f'timeout client {t_default_client}s',
+ f'timeout server {t_default_server}s',
+ )
+ # Check default timeout options
+ config = read_file(HAPROXY_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ # Set custom timeout options
+ self.cli_set(base_path + ['timeout', 'check', t_check])
+ self.cli_set(base_path + ['timeout', 'client', t_client])
+ self.cli_set(base_path + ['timeout', 'connect', t_connect])
+ self.cli_set(base_path + ['timeout', 'server', t_server])
+ self.cli_set(base_path + ['service', haproxy_service_name, 'timeout', 'client', t_front_client])
+
+ self.cli_commit()
+
+ # Check custom timeout options
+ config_entries = (
+ f'timeout check {t_check}s',
+ f'timeout connect {t_connect}s',
+ f'timeout client {t_client}s',
+ f'timeout server {t_server}s',
+ f'timeout client {t_front_client}s',
+ )
+
+ # Check configured options
+ config = read_file(HAPROXY_CONF)
+ for config_entry in config_entries:
+ self.assertIn(config_entry, config)
+
+ def test_reverse_proxy_http_redirect(self):
+ self.base_config()
+ self.cli_set(base_path + ['service', haproxy_service_name, 'redirect-http-to-https'])
+
+ self.cli_commit()
+
+ config = parse_haproxy_config()
+ frontend_name = f'frontend {haproxy_service_name}-http'
+ self.assertIn(frontend_name, config.keys())
+ self.assertIn('mode http', config[frontend_name])
+ self.assertIn('bind [::]:80 v4v6', config[frontend_name])
+ self.assertIn('acl acme_acl path_beg /.well-known/acme-challenge/', config[frontend_name])
+ self.assertIn('use_backend buildin_acme_certbot if acme_acl', config[frontend_name])
+ self.assertIn('redirect scheme https code 301 if !acme_acl', config[frontend_name])
+
+ backend_name = 'backend buildin_acme_certbot'
+ self.assertIn(backend_name, config.keys())
+ port = get_default_port('certbot_haproxy')
+ self.assertIn(f'server localhost 127.0.0.1:{port}', config[backend_name])
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py
index 92b4000b8..32e5f6915 100755
--- a/smoketest/scripts/cli/test_load-balancing_wan.py
+++ b/smoketest/scripts/cli/test_load-balancing_wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright (C) 2022-2025 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,10 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
+import os
import unittest
import time
from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.utils.file import chmod_755
+from vyos.utils.file import write_file
from vyos.utils.process import call
from vyos.utils.process import cmd
@@ -54,6 +57,16 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ removed_chains = [
+ 'wlb_mangle_isp_veth1',
+ 'wlb_mangle_isp_veth2',
+ 'wlb_mangle_isp_eth201',
+ 'wlb_mangle_isp_eth202'
+ ]
+
+ for chain in removed_chains:
+ self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True)
+
def test_table_routes(self):
ns1 = 'ns201'
ns2 = 'ns202'
@@ -93,6 +106,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
cmd_in_netns(ns3, 'ip link set dev eth0 up')
# Set load-balancing configuration
+ self.cli_set(base_path + ['wan', 'hook', '/bin/true'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1'])
self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1'])
@@ -102,7 +116,8 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
-
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2])
# commit changes
self.cli_commit()
@@ -127,7 +142,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
delete_netns(ns3)
def test_check_chains(self):
-
ns1 = 'nsA'
ns2 = 'nsB'
ns3 = 'nsC'
@@ -137,43 +151,28 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
container_iface1 = 'ceth0'
container_iface2 = 'ceth1'
container_iface3 = 'ceth2'
- mangle_isp1 = """table ip mangle {
- chain ISP_veth1 {
- counter ct mark set 0xc9
- counter meta mark set 0xc9
- counter accept
+ mangle_isp1 = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_isp_veth1 {
+ meta mark set 0x000000c9 ct mark set 0x000000c9 counter accept
}
}"""
- mangle_isp2 = """table ip mangle {
- chain ISP_veth2 {
- counter ct mark set 0xca
- counter meta mark set 0xca
- counter accept
+ mangle_isp2 = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_isp_veth2 {
+ meta mark set 0x000000ca ct mark set 0x000000ca counter accept
}
}"""
- mangle_prerouting = """table ip mangle {
- chain PREROUTING {
+ mangle_prerouting = """table ip vyos_wanloadbalance {
+ chain wlb_mangle_prerouting {
type filter hook prerouting priority mangle; policy accept;
- counter jump WANLOADBALANCE_PRE
- }
-}"""
- mangle_wanloadbalance_pre = """table ip mangle {
- chain WANLOADBALANCE_PRE {
- iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1
- iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2
+ iifname "veth3" ip saddr 198.51.100.0/24 ct state new limit rate 5/second burst 5 packets counter numgen random mod 11 vmap { 0 : jump wlb_mangle_isp_veth1, 1-10 : jump wlb_mangle_isp_veth2 }
iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark
}
}"""
- nat_wanloadbalance = """table ip nat {
- chain WANLOADBALANCE {
- ct mark 0xc9 counter snat to 203.0.113.10
- ct mark 0xca counter snat to 192.0.2.10
- }
-}"""
- nat_vyos_pre_snat_hook = """table ip nat {
- chain VYOS_PRE_SNAT_HOOK {
+ nat_wanloadbalance = """table ip vyos_wanloadbalance {
+ chain wlb_nat_postrouting {
type nat hook postrouting priority srcnat - 1; policy accept;
- counter jump WANLOADBALANCE
+ ct mark 0x000000c9 counter snat to 203.0.113.10
+ ct mark 0x000000ca counter snat to 192.0.2.10
}
}"""
@@ -214,7 +213,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3])
self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1])
- self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2, 'weight', '10'])
# commit changes
self.cli_commit()
@@ -222,25 +221,19 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
time.sleep(5)
# Check mangle chains
- tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}')
+ tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface1}')
self.assertEqual(tmp, mangle_isp1)
- tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}')
+ tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface2}')
self.assertEqual(tmp, mangle_isp2)
- tmp = cmd(f'sudo nft -s list chain mangle PREROUTING')
+ tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting')
self.assertEqual(tmp, mangle_prerouting)
- tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE')
- self.assertEqual(tmp, mangle_wanloadbalance_pre)
-
# Check nat chains
- tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE')
+ tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting')
self.assertEqual(tmp, nat_wanloadbalance)
- tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK')
- self.assertEqual(tmp, nat_vyos_pre_snat_hook)
-
# Delete veth interfaces and netns
for iface in [iface1, iface2, iface3]:
call(f'sudo ip link del dev {iface}')
@@ -249,6 +242,85 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase):
delete_netns(ns2)
delete_netns(ns3)
+ def test_criteria_failover_hook(self):
+ isp1_iface = 'eth0'
+ isp2_iface = 'eth1'
+ lan_iface = 'eth2'
+
+ hook_path = '/tmp/wlb_hook.sh'
+ hook_output_path = '/tmp/wlb_hook_output'
+ hook_script = f"""
+#!/bin/sh
+
+ifname=$WLB_INTERFACE_NAME
+state=$WLB_INTERFACE_STATE
+
+echo "$ifname - $state" > {hook_output_path}
+"""
+
+ write_file(hook_path, hook_script)
+ chmod_755(hook_path)
+
+ self.cli_set(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30'])
+ self.cli_set(['interfaces', 'ethernet', isp2_iface, 'address', '192.0.2.2/30'])
+ self.cli_set(['interfaces', 'ethernet', lan_iface, 'address', '198.51.100.2/30'])
+
+ self.cli_set(base_path + ['wan', 'hook', hook_path])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'failure-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'nexthop', '203.0.113.2'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'failure-count', '1'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'nexthop', '192.0.2.2'])
+ self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'success-count', '1'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'exclude'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'inbound-interface', 'eth*'])
+ self.cli_set(base_path + ['wan', 'rule', '5', 'destination', 'address', '10.0.0.0/8'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'failover'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', lan_iface])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'protocol', 'udp'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'port', '53'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'port', '53'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface, 'weight', '10'])
+ self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp2_iface])
+
+ # commit changes
+ self.cli_commit()
+
+ time.sleep(5)
+
+ # Verify isp1 + criteria
+
+ nftables_search = [
+ [f'iifname "eth*"', 'ip daddr 10.0.0.0/8', 'return'],
+ [f'iifname "{lan_iface}"', 'ip saddr 198.51.100.0/24', 'udp sport 53', 'ip daddr 192.0.2.0/24', 'udp dport 53', f'jump wlb_mangle_isp_{isp1_iface}']
+ ]
+
+ self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting')
+
+ # Trigger failure on isp1 health check
+
+ self.cli_delete(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30'])
+ self.cli_commit()
+
+ time.sleep(10)
+
+ # Verify failover to isp2
+
+ nftables_search = [
+ [f'iifname "{lan_iface}"', f'jump wlb_mangle_isp_{isp2_iface}']
+ ]
+
+ self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting')
+
+ # Verify hook output
+
+ self.assertTrue(os.path.exists(hook_output_path))
+
+ with open(hook_output_path, 'r') as f:
+ self.assertIn('eth0 - FAILED', f.read())
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_nat66.py b/smoketest/scripts/cli/test_nat66.py
index 52ad8e3ef..d4b5d6aa4 100755
--- a/smoketest/scripts/cli/test_nat66.py
+++ b/smoketest/scripts/cli/test_nat66.py
@@ -227,6 +227,35 @@ class TestNAT66(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+ def test_source_nat66_network_group(self):
+ address_group = 'smoketest_addr'
+ address_group_member = 'fc00::1'
+ network_group = 'smoketest_net'
+ network_group_member = 'fc00::/64'
+ translation_prefix = 'fc01::/64'
+
+ self.cli_set(['firewall', 'group', 'ipv6-address-group', address_group, 'address', address_group_member])
+ self.cli_set(['firewall', 'group', 'ipv6-network-group', network_group, 'network', network_group_member])
+
+ self.cli_set(src_path + ['rule', '1', 'destination', 'group', 'address-group', address_group])
+ self.cli_set(src_path + ['rule', '1', 'translation', 'address', translation_prefix])
+
+ self.cli_set(src_path + ['rule', '2', 'destination', 'group', 'network-group', network_group])
+ self.cli_set(src_path + ['rule', '2', 'translation', 'address', translation_prefix])
+
+ self.cli_commit()
+
+ nftables_search = [
+ [f'set A6_{address_group}'],
+ [f'elements = {{ {address_group_member} }}'],
+ [f'set N6_{network_group}'],
+ [f'elements = {{ {network_group_member} }}'],
+ ['ip6 daddr', f'@A6_{address_group}', 'snat prefix to fc01::/64'],
+ ['ip6 daddr', f'@N6_{network_group}', 'snat prefix to fc01::/64']
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_nat')
+
def test_nat66_no_rules(self):
# T3206: deleting all rules but keep the direction 'destination' or
# 'source' resulteds in KeyError: 'rule'.
diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py
index 9d4fc0845..985097726 100755
--- a/smoketest/scripts/cli/test_policy.py
+++ b/smoketest/scripts/cli/test_policy.py
@@ -1149,6 +1149,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
},
},
},
+ 'vrf-match': {
+ 'rule': {
+ '10': {
+ 'action': 'permit',
+ 'match': {
+ 'source-vrf': 'TEST',
+ },
+ },
+ },
+ },
}
self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit'])
@@ -1260,6 +1270,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid'])
if 'protocol' in rule_config['match']:
self.cli_set(path + ['rule', rule, 'match', 'protocol', rule_config['match']['protocol']])
+ if 'source-vrf' in rule_config['match']:
+ self.cli_set(path + ['rule', rule, 'match', 'source-vrf', rule_config['match']['source-vrf']])
if 'tag' in rule_config['match']:
self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']])
@@ -1438,6 +1450,9 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase):
if 'rpki-valid' in rule_config['match']:
tmp = f'match rpki valid'
self.assertIn(tmp, config)
+ if 'source-vrf' in rule_config['match']:
+ tmp = f'match source-vrf {rule_config["match"]["source-vrf"]}'
+ self.assertIn(tmp, config)
if 'tag' in rule_config['match']:
tmp = f'match tag {rule_config["match"]["tag"]}'
self.assertIn(tmp, config)
diff --git a/smoketest/scripts/cli/test_policy_route.py b/smoketest/scripts/cli/test_policy_route.py
index 53761b7d6..15ddd857e 100755
--- a/smoketest/scripts/cli/test_policy_route.py
+++ b/smoketest/scripts/cli/test_policy_route.py
@@ -307,5 +307,39 @@ class TestPolicyRoute(VyOSUnitTestSHIM.TestCase):
self.verify_nftables(nftables6_search, 'ip6 vyos_mangle')
+ def test_geoip(self):
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'se'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '1', 'source', 'geoip', 'country-code', 'gb'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'action', 'accept'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'de'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'country-code', 'fr'])
+ self.cli_set(['policy', 'route', 'smoketest', 'rule', '2', 'source', 'geoip', 'inverse-match'])
+
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'action', 'drop'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'se'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '1', 'source', 'geoip', 'country-code', 'gb'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'action', 'accept'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'de'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'country-code', 'fr'])
+ self.cli_set(['policy', 'route6', 'smoketest6', 'rule', '2', 'source', 'geoip', 'inverse-match'])
+
+ self.cli_commit()
+
+ nftables_search = [
+ ['ip saddr @GEOIP_CC_route_smoketest_1', 'drop'],
+ ['ip saddr != @GEOIP_CC_route_smoketest_2', 'accept'],
+ ]
+
+ # -t prevents 1000+ GeoIP elements being returned
+ self.verify_nftables(nftables_search, 'ip vyos_mangle', args='-t')
+
+ nftables_search = [
+ ['ip6 saddr @GEOIP_CC6_route6_smoketest6_1', 'drop'],
+ ['ip6 saddr != @GEOIP_CC6_route6_smoketest6_2', 'accept'],
+ ]
+
+ self.verify_nftables(nftables_search, 'ip6 vyos_mangle', args='-t')
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py
index 7ecf54600..3a9ee2d62 100755
--- a/smoketest/scripts/cli/test_protocols_babel.py
+++ b/smoketest/scripts/cli/test_protocols_babel.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright (C) 2024-2025 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
@@ -72,7 +72,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig)
def test_02_redistribute(self):
- ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'rip', 'static']
+ ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'rip', 'static']
ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static']
self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps'])
diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py
index 761eb8bfe..8403dcc37 100755
--- a/smoketest/scripts/cli/test_protocols_bgp.py
+++ b/smoketest/scripts/cli/test_protocols_bgp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -655,10 +655,71 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
}
# We want to redistribute ...
- redistributes = ['connected', 'isis', 'kernel', 'ospf', 'rip', 'static']
- for redistribute in redistributes:
- self.cli_set(base_path + ['address-family', 'ipv4-unicast',
- 'redistribute', redistribute])
+ redistributes = {
+ 'babel' : {
+ 'metric' : '100',
+ 'route_map' : 'redistr-ipv4-babel',
+ },
+ 'connected' : {
+ 'metric' : '200',
+ 'route_map' : 'redistr-ipv4-connected',
+ },
+ 'isis' : {
+ 'metric' : '300',
+ 'route_map' : 'redistr-ipv4-isis',
+ },
+ 'kernel' : {
+ 'metric' : '400',
+ 'route_map' : 'redistr-ipv4-kernel',
+ },
+ 'nhrp': {
+ 'metric': '400',
+ 'route_map': 'redistr-ipv4-nhrp',
+ },
+ 'ospf' : {
+ 'metric' : '500',
+ 'route_map' : 'redistr-ipv4-ospf',
+ },
+ 'rip' : {
+ 'metric' : '600',
+ 'route_map' : 'redistr-ipv4-rip',
+ },
+ 'static' : {
+ 'metric' : '700',
+ 'route_map' : 'redistr-ipv4-static',
+ },
+ 'table' : {
+ '10' : {
+ 'metric' : '810',
+ 'route_map' : 'redistr-ipv4-table-10',
+ },
+ '20' : {
+ 'metric' : '820',
+ 'route_map' : 'redistr-ipv4-table-20',
+ },
+ '30' : {
+ 'metric' : '830',
+ 'route_map' : 'redistr-ipv4-table-30',
+ },
+ },
+ }
+ for proto, proto_config in redistributes.items():
+ proto_path = base_path + ['address-family', 'ipv4-unicast', 'redistribute', proto]
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ self.cli_set(proto_path + [table])
+ if 'metric' in table_config:
+ self.cli_set(proto_path + [table, 'metric'], value=table_config['metric'])
+ if 'route_map' in table_config:
+ self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit')
+ self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map'])
+ else:
+ self.cli_set(proto_path)
+ if 'metric' in proto_config:
+ self.cli_set(proto_path + ['metric', proto_config['metric']])
+ if 'route_map' in proto_config:
+ self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '10', 'action', 'permit'])
+ self.cli_set(proto_path + ['route-map', proto_config['route_map']])
for network, network_config in networks.items():
self.cli_set(base_path + ['address-family', 'ipv4-unicast',
@@ -679,10 +740,29 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
# Verify FRR bgpd configuration
frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
- self.assertIn(f' address-family ipv4 unicast', frrconfig)
-
- for redistribute in redistributes:
- self.assertIn(f' redistribute {redistribute}', frrconfig)
+ self.assertIn(' address-family ipv4 unicast', frrconfig)
+
+ for proto, proto_config in redistributes.items():
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ tmp = f' redistribute table-direct {table}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+ self.assertIn(tmp, frrconfig)
+ else:
+ tmp = f' redistribute {proto}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+
+ self.assertIn(tmp, frrconfig)
for network, network_config in networks.items():
self.assertIn(f' network {network}', frrconfig)
@@ -695,6 +775,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
command = f'{command} route-map {network_config["route_map"]}'
self.assertIn(command, frrconfig)
+ for proto, proto_config in redistributes.items():
+ if 'route_map' in proto_config:
+ self.cli_delete(['policy', 'route-map', proto_config['route_map']])
+
def test_bgp_05_afi_ipv6(self):
networks = {
'2001:db8:100::/48' : {
@@ -707,10 +791,67 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
}
# We want to redistribute ...
- redistributes = ['connected', 'kernel', 'ospfv3', 'ripng', 'static']
- for redistribute in redistributes:
- self.cli_set(base_path + ['address-family', 'ipv6-unicast',
- 'redistribute', redistribute])
+ redistributes = {
+ 'babel' : {
+ 'metric' : '100',
+ 'route_map' : 'redistr-ipv6-babel',
+ },
+ 'connected' : {
+ 'metric' : '200',
+ 'route_map' : 'redistr-ipv6-connected',
+ },
+ 'isis' : {
+ 'metric' : '300',
+ 'route_map' : 'redistr-ipv6-isis',
+ },
+ 'kernel' : {
+ 'metric' : '400',
+ 'route_map' : 'redistr-ipv6-kernel',
+ },
+ 'ospfv3' : {
+ 'metric' : '500',
+ 'route_map' : 'redistr-ipv6-ospfv3',
+ },
+ 'ripng' : {
+ 'metric' : '600',
+ 'route_map' : 'redistr-ipv6-ripng',
+ },
+ 'static' : {
+ 'metric' : '700',
+ 'route_map' : 'redistr-ipv6-static',
+ },
+ 'table' : {
+ '110' : {
+ 'metric' : '811',
+ 'route_map' : 'redistr-ipv6-table-110',
+ },
+ '120' : {
+ 'metric' : '821',
+ 'route_map' : 'redistr-ipv6-table-120',
+ },
+ '130' : {
+ 'metric' : '831',
+ 'route_map' : 'redistr-ipv6-table-130',
+ },
+ },
+ }
+ for proto, proto_config in redistributes.items():
+ proto_path = base_path + ['address-family', 'ipv6-unicast', 'redistribute', proto]
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ self.cli_set(proto_path + [table])
+ if 'metric' in table_config:
+ self.cli_set(proto_path + [table, 'metric'], value=table_config['metric'])
+ if 'route_map' in table_config:
+ self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit')
+ self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map'])
+ else:
+ self.cli_set(proto_path)
+ if 'metric' in proto_config:
+ self.cli_set(proto_path + ['metric', proto_config['metric']])
+ if 'route_map' in proto_config:
+ self.cli_set(['policy', 'route-map', proto_config['route_map'], 'rule', '20', 'action', 'permit'])
+ self.cli_set(proto_path + ['route-map', proto_config['route_map']])
for network, network_config in networks.items():
self.cli_set(base_path + ['address-family', 'ipv6-unicast',
@@ -725,22 +866,45 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
# Verify FRR bgpd configuration
frrconfig = self.getFRRconfig(f'router bgp {ASN}', endsection='^exit')
self.assertIn(f'router bgp {ASN}', frrconfig)
- self.assertIn(f' address-family ipv6 unicast', frrconfig)
+ self.assertIn(' address-family ipv6 unicast', frrconfig)
# T2100: By default ebgp-requires-policy is disabled to keep VyOS
# 1.3 and 1.2 backwards compatibility
- self.assertIn(f' no bgp ebgp-requires-policy', frrconfig)
-
- for redistribute in redistributes:
- # FRR calls this OSPF6
- if redistribute == 'ospfv3':
- redistribute = 'ospf6'
- self.assertIn(f' redistribute {redistribute}', frrconfig)
+ self.assertIn(' no bgp ebgp-requires-policy', frrconfig)
+
+ for proto, proto_config in redistributes.items():
+ if proto == 'table':
+ for table, table_config in proto_config.items():
+ tmp = f' redistribute table-direct {table}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+ self.assertIn(tmp, frrconfig)
+ else:
+ # FRR calls this OSPF6
+ if proto == 'ospfv3':
+ proto = 'ospf6'
+ tmp = f' redistribute {proto}'
+ if 'metric' in proto_config:
+ metric = proto_config['metric']
+ tmp += f' metric {metric}'
+ if 'route_map' in proto_config:
+ route_map = proto_config['route_map']
+ tmp += f' route-map {route_map}'
+
+ self.assertIn(tmp, frrconfig)
for network, network_config in networks.items():
self.assertIn(f' network {network}', frrconfig)
if 'as_set' in network_config:
self.assertIn(f' aggregate-address {network} summary-only', frrconfig)
+ for proto, proto_config in redistributes.items():
+ if 'route_map' in proto_config:
+ self.cli_delete(['policy', 'route-map', proto_config['route_map']])
+
def test_bgp_06_listen_range(self):
# Implemented via T1875
limit = '64'
@@ -1376,6 +1540,42 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase):
self.assertIn(f'neighbor OVERLAY remote-as {int(ASN) + 1}', conf)
self.assertIn(f'neighbor OVERLAY local-as {int(ASN) + 1}', conf)
+ def test_bgp_30_import_vrf_routemap(self):
+ router_id = '127.0.0.3'
+ table = '1000'
+ vrf = 'red'
+ vrf_base = ['vrf', 'name', vrf]
+ self.cli_set(vrf_base + ['table', table])
+ self.cli_set(vrf_base + ['protocols', 'bgp', 'system-as', ASN])
+ self.cli_set(
+ vrf_base + ['protocols', 'bgp', 'parameters', 'router-id',
+ router_id])
+
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'import',
+ 'vrf', vrf])
+ self.cli_set(
+ base_path + ['address-family', 'ipv4-unicast', 'route-map',
+ 'vrf', 'import', route_map_in])
+
+ self.cli_commit()
+
+ # Verify FRR bgpd configuration
+ frrconfig = self.getFRRconfig(f'router bgp {ASN}',
+ endsection='^exit')
+ self.assertIn(f'router bgp {ASN}', frrconfig)
+ self.assertIn(f' address-family ipv4 unicast', frrconfig)
+
+ self.assertIn(f' import vrf {vrf}', frrconfig)
+ self.assertIn(f' import vrf route-map {route_map_in}', frrconfig)
+
+ # Verify FRR bgpd configuration
+ frr_vrf_config = self.getFRRconfig(
+ f'router bgp {ASN} vrf {vrf}', endsection='^exit')
+ self.assertIn(f'router bgp {ASN} vrf {vrf}', frr_vrf_config)
+ self.assertIn(f' bgp router-id {router_id}', frr_vrf_config)
+
+
def test_bgp_99_bmp(self):
target_name = 'instance-bmp'
target_address = '127.0.0.1'
diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py
index 598250d28..14e833fd9 100755
--- a/smoketest/scripts/cli/test_protocols_isis.py
+++ b/smoketest/scripts/cli/test_protocols_isis.py
@@ -59,7 +59,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
route_map = 'EXPORT-ISIS'
rule = '10'
metric_style = 'transition'
-
+ redistribute = ['babel', 'bgp', 'connected', 'kernel', 'nhrp', 'ospf', 'rip', 'static']
self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit'])
self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24'])
self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit'])
@@ -80,7 +80,9 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError):
self.cli_commit()
- self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map])
+ for proto in redistribute:
+ self.cli_set(base_path + ['redistribute', 'ipv4', proto, 'level-2', 'route-map', route_map])
+
self.cli_set(base_path + ['metric-style', metric_style])
self.cli_set(base_path + ['log-adjacency-changes'])
@@ -92,7 +94,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase):
self.assertIn(f' net {net}', tmp)
self.assertIn(f' metric-style {metric_style}', tmp)
self.assertIn(f' log-adjacency-changes', tmp)
- self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp)
+ for proto in redistribute:
+ self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp)
for interface in self._interfaces:
tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit')
diff --git a/smoketest/scripts/cli/test_protocols_mpls.py b/smoketest/scripts/cli/test_protocols_mpls.py
index 654f2f099..3840c24f4 100755
--- a/smoketest/scripts/cli/test_protocols_mpls.py
+++ b/smoketest/scripts/cli/test_protocols_mpls.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -121,5 +121,74 @@ class TestProtocolsMPLS(VyOSUnitTestSHIM.TestCase):
for interface in interfaces:
self.assertIn(f' interface {interface}', afiv4_config)
+ def test_02_mpls_disable_establish_hello(self):
+ router_id = '1.2.3.4'
+ transport_ipv4_addr = '5.6.7.8'
+ transport_ipv6_addr = '2001:db8:1111::1111'
+ interfaces = Section.interfaces('ethernet')
+
+ self.cli_set(base_path + ['router-id', router_id])
+
+ # At least one LDP interface must be configured
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # LDP transport address missing
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(base_path + ['discovery', 'transport-ipv4-address', transport_ipv4_addr])
+ self.cli_set(base_path + ['discovery', 'transport-ipv6-address', transport_ipv6_addr])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate configuration
+ frrconfig = self.getFRRconfig('mpls ldp', endsection='^exit')
+ self.assertIn(f'mpls ldp', frrconfig)
+ self.assertIn(f' router-id {router_id}', frrconfig)
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv4',
+ endsubsection='^ exit-address-family')
+ self.assertIn(f' discovery transport-address {transport_ipv4_addr}', afiv4_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+ self.assertIn(f' disable-establish-hello', afiv4_config)
+
+ # Validate AFI IPv6
+ afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv6',
+ endsubsection='^ exit-address-family')
+ self.assertIn(f' discovery transport-address {transport_ipv6_addr}', afiv6_config)
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv6_config)
+ self.assertIn(f' disable-establish-hello', afiv6_config)
+
+ # Delete disable-establish-hello
+ for interface in interfaces:
+ self.cli_delete(base_path + ['interface', interface, 'disable-establish-hello'])
+
+ # Commit changes
+ self.cli_commit()
+
+ # Validate AFI IPv4
+ afiv4_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv4',
+ endsubsection='^ exit-address-family')
+ # Validate AFI IPv6
+ afiv6_config = self.getFRRconfig('mpls ldp', endsection='^exit',
+ substring=' address-family ipv6',
+ endsubsection='^ exit-address-family')
+ # Check deleted 'disable-establish-hello' option per interface
+ for interface in interfaces:
+ self.assertIn(f' interface {interface}', afiv4_config)
+ self.assertNotIn(f' disable-establish-hello', afiv4_config)
+ self.assertIn(f' interface {interface}', afiv6_config)
+ self.assertNotIn(f' disable-establish-hello', afiv6_config)
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_protocols_nhrp.py b/smoketest/scripts/cli/test_protocols_nhrp.py
index f6d1f1da5..73a760945 100755
--- a/smoketest/scripts/cli/test_protocols_nhrp.py
+++ b/smoketest/scripts/cli/test_protocols_nhrp.py
@@ -17,10 +17,7 @@
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
-
-from vyos.firewall import find_nftables_rule
from vyos.utils.process import process_named_running
-from vyos.utils.file import read_file
tunnel_path = ['interfaces', 'tunnel']
nhrp_path = ['protocols', 'nhrp']
diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py
index 77882737f..ea55fa031 100755
--- a/smoketest/scripts/cli/test_protocols_ospf.py
+++ b/smoketest/scripts/cli/test_protocols_ospf.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -255,7 +255,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase):
def test_ospf_07_redistribute(self):
metric = '15'
metric_type = '1'
- redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static']
+ redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static']
for protocol in redistribute:
self.cli_set(base_path + ['redistribute', protocol, 'metric', metric])
diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py
index 671ef8cd5..27b543803 100755
--- a/smoketest/scripts/cli/test_protocols_rip.py
+++ b/smoketest/scripts/cli/test_protocols_rip.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2023 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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
@@ -82,7 +82,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase):
interfaces = Section.interfaces('ethernet')
neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6']
networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16']
- redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static']
+ redistribute = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'static']
timer_garbage = '888'
timer_timeout = '1000'
timer_update = '90'
diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py
index ef2f30d3e..0addf7fee 100755
--- a/smoketest/scripts/cli/test_protocols_rpki.py
+++ b/smoketest/scripts/cli/test_protocols_rpki.py
@@ -248,5 +248,41 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase):
with self.assertRaises(ConfigSessionError):
self.cli_commit()
+ def test_rpki_source_address(self):
+ peer = '192.0.2.1'
+ port = '8080'
+ preference = '1'
+ username = 'foo'
+ source_address = '100.10.10.1'
+
+ self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', f'{source_address}/24'])
+
+ # Configure a TCP cache server
+ self.cli_set(base_path + ['cache', peer, 'port', port])
+ self.cli_set(base_path + ['cache', peer, 'preference', preference])
+ self.cli_set(base_path + ['cache', peer, 'source-address', source_address])
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('rpki')
+ self.assertIn(f'rpki cache tcp {peer} {port} source {source_address} preference {preference}', frrconfig)
+
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n', '')])
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n', '')])
+ self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type])
+
+ # Configure a SSH cache server
+ self.cli_set(base_path + ['cache', peer, 'ssh', 'username', username])
+ self.cli_set(base_path + ['cache', peer, 'ssh', 'key', rpki_key_name])
+ self.cli_commit()
+
+ # Verify FRR configuration
+ frrconfig = self.getFRRconfig('rpki')
+ self.assertIn(
+ f'rpki cache ssh {peer} {port} {username} /run/frr/id_rpki_{peer} /run/frr/id_rpki_{peer}.pub source {source_address} preference {preference}',
+ frrconfig,
+ )
+
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py
index f891bf295..e421f04d2 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-2024 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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/>.
import os
+import re
import unittest
from json import loads
@@ -22,15 +23,21 @@ from json import loads
from base_vyostest_shim import VyOSUnitTestSHIM
from vyos.configsession import ConfigSessionError
+from vyos.kea import kea_add_lease
+from vyos.kea import kea_delete_lease
+from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
from vyos.utils.file import read_file
from vyos.template import inc_ip
from vyos.template import dec_ip
PROCESS_NAME = 'kea-dhcp4'
+D2_PROCESS_NAME = 'kea-dhcp-ddns'
CTRL_PROCESS_NAME = 'kea-ctrl-agent'
KEA4_CONF = '/run/kea/kea-dhcp4.conf'
+KEA4_D2_CONF = '/run/kea/kea-dhcp-ddns.conf'
KEA4_CTRL = '/run/kea/dhcp4-ctrl-socket'
+HOSTSD_CLIENT = '/usr/bin/vyos-hostsd-client'
base_path = ['service', 'dhcp-server']
interface = 'dum8765'
subnet = '192.0.2.0/25'
@@ -39,15 +46,18 @@ dns_1 = inc_ip(subnet, 2)
dns_2 = inc_ip(subnet, 3)
domain_name = 'vyos.net'
+
class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
@classmethod
def setUpClass(cls):
super(TestServiceDHCPServer, cls).setUpClass()
- # Clear out current configuration to allow running this test on a live system
+ # Clear out current configuration to allow running this test on a live system
cls.cli_delete(cls, base_path)
cidr_mask = subnet.split('/')[-1]
- cls.cli_set(cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}'])
+ cls.cli_set(
+ cls, ['interfaces', 'dummy', interface, 'address', f'{router}/{cidr_mask}']
+ )
@classmethod
def tearDownClass(cls):
@@ -69,7 +79,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.assertTrue(isinstance(current, list), msg=f'Failed path: {path}')
self.assertTrue(0 <= key < len(current), msg=f'Failed path: {path}')
else:
- assert False, "Invalid type"
+ assert False, 'Invalid type'
current = current[key]
@@ -88,19 +98,26 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
self.assertTrue(key in base_obj)
self.assertEqual(base_obj[key], value)
+ def verify_service_running(self):
+ tmp = cmd('tail -n 100 /var/log/messages | grep kea')
+ self.assertTrue(process_named_running(PROCESS_NAME), msg=f'Service not running, log: {tmp}')
+
def test_dhcp_single_pool_range(self):
shared_net_name = 'SMOKE-1'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 40)
- range_1_stop = inc_ip(subnet, 50)
+ range_1_stop = inc_ip(subnet, 50)
self.cli_set(base_path + ['listen-interface', interface])
+ self.cli_set(base_path + ['shared-network-name', shared_net_name, 'ping-check'])
+
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
self.cli_set(pool + ['ignore-client-id'])
+ self.cli_set(pool + ['ping-check'])
# we use the first subnet IP address as default gateway
self.cli_set(pool + ['option', 'default-router', router])
self.cli_set(pool + ['option', 'name-server', dns_1])
@@ -121,55 +138,90 @@ 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'], 'match-client-id', False)
- 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)
+ 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'], 'match-client-id', False
+ )
+ 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 ping-check
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'user-context'],
+ 'enable-ping-check',
+ True
+ )
+
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'user-context'],
+ 'enable-ping-check',
+ True
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_1_start} - {range_1_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_1_start} - {range_1_stop}'},
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_single_pool_options(self):
shared_net_name = 'SMOKE-0815'
- range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
- smtp_server = '1.2.3.4'
- time_server = '4.3.2.1'
- tftp_server = 'tftp.vyos.io'
- search_domains = ['foo.vyos.net', 'bar.vyos.net']
- bootfile_name = 'vyos'
- bootfile_server = '192.0.2.1'
- wpad = 'http://wpad.vyos.io/foo/bar'
- server_identifier = bootfile_server
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+ smtp_server = '1.2.3.4'
+ time_server = '4.3.2.1'
+ tftp_server = 'tftp.vyos.io'
+ search_domains = ['foo.vyos.net', 'bar.vyos.net']
+ bootfile_name = 'vyos'
+ bootfile_server = '192.0.2.1'
+ wpad = 'http://wpad.vyos.io/foo/bar'
+ server_identifier = bootfile_server
ipv6_only_preferred = '300'
+ capwap_access_controller = '192.168.2.125'
pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -189,8 +241,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
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 + ['option', 'capwap-controller', capwap_access_controller]
+ )
+
+ static_route = '10.0.0.0/24'
+ static_route_nexthop = '192.0.2.1'
- self.cli_set(pool + ['option', 'static-route', '10.0.0.0/24', 'next-hop', '192.0.2.1'])
+ self.cli_set(
+ pool + ['option', 'static-route', static_route, 'next-hop', static_route_nexthop]
+ )
self.cli_set(pool + ['option', 'ipv6-only-preferred', ipv6_only_preferred])
self.cli_set(pool + ['option', 'time-zone', 'Europe/London'])
@@ -203,95 +263,133 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
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'], 'boot-file-name', bootfile_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'next-server', bootfile_server)
- 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)
+ 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'],
+ 'boot-file-name',
+ bootfile_name,
+ )
+ self.verify_config_value(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4'],
+ 'next-server',
+ bootfile_server,
+ )
+ 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 options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-search', 'data': ', '.join(search_domains)})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-search', 'data': ', '.join(search_domains)},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'pop-server', 'data': smtp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pop-server', 'data': smtp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'smtp-server', 'data': smtp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'smtp-server', 'data': smtp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'time-servers', 'data': time_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'time-servers', 'data': time_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'dhcp-server-identifier', 'data': server_identifier})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'dhcp-server-identifier', 'data': server_identifier},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'tftp-server-name', 'data': tftp_server})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'capwap-ac-v4', 'data': capwap_access_controller},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'wpad-url', 'data': wpad})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tftp-server-name', 'data': tftp_server},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'rfc3442-static-route', 'data': '24,10,0,0,192,0,2,1, 0,192,0,2,1'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'wpad-url', 'data': wpad},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'windows-static-route', 'data': '24,10,0,0,192,0,2,1'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {
+ 'name': 'classless-static-route',
+ 'data': f'{static_route} - {static_route_nexthop}, 0.0.0.0/0 - {router}',
+ },
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'v6-only-preferred', 'data': ipv6_only_preferred})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'v6-only-preferred', 'data': ipv6_only_preferred},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'ip-forwarding', 'data': "true"})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'ip-forwarding', 'data': 'true'},
+ )
# Time zone
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'pcode', 'data': 'GMT0BST,M3.5.0/1,M10.5.0'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'tcode', 'data': 'Europe/London'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'tcode', 'data': 'Europe/London'},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ 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))
+ self.verify_service_running()
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_0_stop = inc_ip(subnet, 20)
range_router = inc_ip(subnet, 5)
range_dns_1 = inc_ip(subnet, 6)
@@ -320,40 +418,58 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
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)
+ 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})
+ 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}'})
+ 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})
+ 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}'})
+ 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})
+ 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}')
+ 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))
+ self.verify_service_running()
def test_dhcp_single_pool_static_mapping(self):
shared_net_name = 'SMOKE-2'
@@ -375,18 +491,31 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
for client in ['client1', 'client2', 'client3']:
mac = '00:50:00:00:00:{}'.format(client_base)
self.cli_set(pool + ['static-mapping', client, 'mac', mac])
- self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ self.cli_set(
+ pool
+ + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)]
+ )
client_base += 1
# cannot have both mac-address and duid set
with self.assertRaises(ConfigSessionError):
- self.cli_set(pool + ['static-mapping', 'client1', 'duid', '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11'])
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ 'client1',
+ 'duid',
+ '00:01:00:01:12:34:56:78:aa:bb:cc:dd:ee:11',
+ ]
+ )
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)])
+ self.cli_set(
+ pool + ['static-mapping', 'dupe1', 'ip-address', inc_ip(subnet, 10)]
+ )
with self.assertRaises(ConfigSessionError):
self.cli_commit()
# Should allow disabled duplicate
@@ -396,17 +525,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
# 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)])
+ 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)])
+ 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'])
@@ -418,25 +568,38 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
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'], '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)
+ 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
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': f'{dns_1}, {dns_2}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
client_base = 10
for client in ['client1', 'client2', 'client3']:
@@ -444,14 +607,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
ip = inc_ip(subnet, client_base)
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
- {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'reservations'],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip},
+ )
client_base += 1
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_multiple_pools(self):
lease_time = '14400'
@@ -463,11 +627,16 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
- range_1_stop = inc_ip(subnet, 40)
-
- pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ 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 + ['option', 'default-router', router])
@@ -484,7 +653,15 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
for client in ['client1', 'client2', 'client3', 'client4']:
mac = '02:50:00:00:00:{}'.format(client_base)
self.cli_set(pool + ['static-mapping', client, 'mac', mac])
- self.cli_set(pool + ['static-mapping', client, 'ip-address', inc_ip(subnet, client_base)])
+ self.cli_set(
+ pool
+ + [
+ 'static-mapping',
+ client,
+ 'ip-address',
+ inc_ip(subnet, client_base),
+ ]
+ )
client_base += 1
# commit changes
@@ -500,37 +677,64 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
dns_1 = inc_ip(subnet, 2)
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
range_1_start = inc_ip(subnet, 30)
- range_1_stop = inc_ip(subnet, 40)
+ range_1_stop = inc_ip(subnet, 40)
- 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))
+ 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),
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'domain-name', 'data': domain_name})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'domain-name-servers', 'data': dns_1})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name-servers', 'data': dns_1},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
- {'pool': f'{range_1_start} - {range_1_stop}'})
+ obj,
+ ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'pools'],
+ {'pool': f'{range_1_start} - {range_1_stop}'},
+ )
client_base = 60
for client in ['client1', 'client2', 'client3', 'client4']:
@@ -538,20 +742,28 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
ip = inc_ip(subnet, client_base)
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', int(network), 'subnet4', 0, 'reservations'],
- {'hostname': client, 'hw-address': mac, 'ip-address': ip})
+ obj,
+ [
+ 'Dhcp4',
+ 'shared-networks',
+ int(network),
+ 'subnet4',
+ 0,
+ 'reservations',
+ ],
+ {'hostname': client, 'hw-address': mac, 'ip-address': ip},
+ )
client_base += 1
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_exclude_not_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ range_0_stop = inc_ip(subnet, 20)
pool = base_path + ['shared-network-name', 'EXCLUDE-TEST', 'subnet', subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -567,38 +779,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST')
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST'
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
pool_obj = {
'pool': f'{range_0_start} - {range_0_stop}',
- 'option-data': [{'name': 'routers', 'data': router}]
+ 'option-data': [{'name': 'routers', 'data': router}],
}
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- pool_obj)
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_exclude_in_range(self):
# T3180: verify else path when slicing DHCP ranges and exclude address
# is not part of the DHCP range
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 100)
+ range_0_stop = inc_ip(subnet, 100)
# the DHCP exclude addresse is blanked out of the range which is done
# by slicing one range into two ranges
- exclude_addr = inc_ip(range_0_start, 20)
+ exclude_addr = inc_ip(range_0_start, 20)
range_0_stop_excl = dec_ip(exclude_addr, 1)
range_0_start_excl = inc_ip(exclude_addr, 1)
@@ -616,37 +832,42 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2')
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', 'EXCLUDE-TEST-2'
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
pool_obj = {
'pool': f'{range_0_start} - {range_0_stop_excl}',
- 'option-data': [{'name': 'routers', 'data': router}]
+ 'option-data': [{'name': 'routers', 'data': router}],
}
pool_exclude_obj = {
'pool': f'{range_0_start_excl} - {range_0_stop}',
- 'option-data': [{'name': 'routers', 'data': router}]
+ 'option-data': [{'name': 'routers', 'data': router}],
}
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- pool_obj)
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'], pool_obj
+ )
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- pool_exclude_obj)
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ pool_exclude_obj,
+ )
# Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_relay_server(self):
# Listen on specific address and return DHCP leases from a non
@@ -657,7 +878,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
relay_router = inc_ip(relay_subnet, 1)
range_0_start = '10.0.1.0'
- range_0_stop = '10.0.250.255'
+ range_0_stop = '10.0.250.255'
pool = base_path + ['shared-network-name', 'RELAY', 'subnet', relay_subnet]
self.cli_set(pool + ['subnet-id', '1'])
@@ -671,31 +892,37 @@ 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', '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)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', relay_subnet
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': relay_router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': relay_router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ 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))
+ self.verify_service_running()
def test_dhcp_high_availability(self):
shared_net_name = 'FAILOVER'
failover_name = 'VyOS-Failover'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ 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'])
@@ -712,7 +939,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
failover_local = router
failover_remote = inc_ip(router, 1)
- self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(
+ base_path + ['high-availability', 'source-address', failover_local]
+ )
self.cli_set(base_path + ['high-availability', 'name', failover_name])
self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
self.cli_set(base_path + ['high-availability', 'status', 'primary'])
@@ -725,43 +954,78 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
obj = loads(config)
# Verify failover
- self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'primary', 'auto-failover': True})
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': os.uname()[1],
+ 'url': f'http://{failover_local}:647/',
+ 'role': 'primary',
+ 'auto-failover': True,
+ },
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'secondary', 'auto-failover': True})
-
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
- self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet)
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': failover_name,
+ 'url': f'http://{failover_remote}:647/',
+ 'role': 'secondary',
+ 'auto-failover': True,
+ },
+ )
+
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
# Verify options
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
# Verify pools
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ 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))
- self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
+ self.verify_service_running()
def test_dhcp_high_availability_standby(self):
shared_net_name = 'FAILOVER'
failover_name = 'VyOS-Failover'
range_0_start = inc_ip(subnet, 10)
- range_0_stop = inc_ip(subnet, 20)
+ 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'])
@@ -774,7 +1038,9 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
failover_local = router
failover_remote = inc_ip(router, 1)
- self.cli_set(base_path + ['high-availability', 'source-address', failover_local])
+ self.cli_set(
+ base_path + ['high-availability', 'source-address', failover_local]
+ )
self.cli_set(base_path + ['high-availability', 'name', failover_name])
self.cli_set(base_path + ['high-availability', 'remote', failover_remote])
self.cli_set(base_path + ['high-availability', 'status', 'secondary'])
@@ -787,61 +1053,383 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase):
obj = loads(config)
# Verify failover
- self.verify_config_value(obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL)
+ self.verify_config_value(
+ obj, ['Dhcp4', 'control-socket'], 'socket-name', KEA4_CTRL
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': os.uname()[1], 'url': f'http://{failover_local}:647/', 'role': 'standby', 'auto-failover': True})
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': os.uname()[1],
+ 'url': f'http://{failover_local}:647/',
+ 'role': 'standby',
+ 'auto-failover': True,
+ },
+ )
self.verify_config_object(
obj,
- ['Dhcp4', 'hooks-libraries', 0, 'parameters', 'high-availability', 0, 'peers'],
- {'name': failover_name, 'url': f'http://{failover_remote}:647/', 'role': 'primary', 'auto-failover': True})
+ [
+ 'Dhcp4',
+ 'hooks-libraries',
+ 0,
+ 'parameters',
+ 'high-availability',
+ 0,
+ 'peers',
+ ],
+ {
+ 'name': failover_name,
+ 'url': f'http://{failover_remote}:647/',
+ 'role': 'primary',
+ 'auto-failover': True,
+ },
+ )
+
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name
+ )
+ self.verify_config_value(
+ obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'subnet', subnet
+ )
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'routers', 'data': router},
+ )
+
+ # Verify pools
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{range_0_start} - {range_0_stop}'},
+ )
+
+ # Check for running process
+ self.verify_service_running()
+
+ def test_dhcp_dynamic_dns_update(self):
+ shared_net_name = 'SMOKE-1DDNS'
+
+ range_0_start = inc_ip(subnet, 10)
+ range_0_stop = inc_ip(subnet, 20)
+
+ self.cli_set(base_path + ['listen-interface', interface])
+
+ ddns = base_path + ['dynamic-dns-update']
+
+ self.cli_set(ddns + ['send-updates', 'enable'])
+ self.cli_set(ddns + ['conflict-resolution', 'enable'])
+ self.cli_set(ddns + ['override-no-update', 'enable'])
+ self.cli_set(ddns + ['override-client-update', 'enable'])
+ self.cli_set(ddns + ['replace-client-name', 'always'])
+ self.cli_set(ddns + ['update-on-renew', 'enable'])
+
+ self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'algorithm', 'sha256'])
+ self.cli_set(ddns + ['tsig-key', 'domain-lan-updates', 'secret', 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='])
+ self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'algorithm', 'sha256'])
+ self.cli_set(ddns + ['tsig-key', 'reverse-0-168-192', 'secret', 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '1', 'address', '192.168.0.1'])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'dns-server', '2', 'address', '100.100.0.1'])
+ self.cli_set(ddns + ['forward-domain', 'domain.lan', 'key-name', 'domain-lan-updates'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'address', '192.168.0.1'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '1', 'port', '1053'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'address', '100.100.0.1'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'dns-server', '2', 'port', '1153'])
+ self.cli_set(ddns + ['reverse-domain', '0.168.192.in-addr.arpa', 'key-name', 'reverse-0-168-192'])
+
+ shared = base_path + ['shared-network-name', shared_net_name]
+
+ self.cli_set(shared + ['dynamic-dns-update', 'send-updates', 'enable'])
+ self.cli_set(shared + ['dynamic-dns-update', 'conflict-resolution', 'enable'])
+ self.cli_set(shared + ['dynamic-dns-update', 'ttl-percent', '75'])
+
+ pool = shared + [ 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+
+ self.cli_set(pool + ['range', '0', 'start', range_0_start])
+ self.cli_set(pool + ['range', '0', 'stop', range_0_stop])
+
+ self.cli_set(pool + ['dynamic-dns-update', 'send-updates', 'enable'])
+ self.cli_set(pool + ['dynamic-dns-update', 'generated-prefix', 'myfunnyprefix'])
+ self.cli_set(pool + ['dynamic-dns-update', 'qualifying-suffix', 'suffix.lan'])
+ self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-set', 'xXyYzZ'])
+ self.cli_set(pool + ['dynamic-dns-update', 'hostname-char-replacement', '_xXx_'])
+
+ self.cli_commit()
+
+ config = read_file(KEA4_CONF)
+ d2_config = read_file(KEA4_D2_CONF)
+
+ obj = loads(config)
+ d2_obj = loads(d2_config)
+
+ # Verify global DDNS parameters in the main config file
+ self.verify_config_value(
+ obj,
+ ['Dhcp4'], 'dhcp-ddns',
+ {'enable-updates': True, 'server-ip': '127.0.0.1', 'server-port': 53001, 'sender-ip': '', 'sender-port': 0,
+ 'max-queue-size': 1024, 'ncr-protocol': 'UDP', 'ncr-format': 'JSON'})
+
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-use-conflict-resolution', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-no-update', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-override-client-update', True)
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-replace-client-name', 'always')
+ self.verify_config_value(obj, ['Dhcp4'], 'ddns-update-on-renew', True)
+
+ # Verify scoped DDNS parameters in the main config file
self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'name', shared_net_name)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-use-conflict-resolution', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks'], 'ddns-ttl-percent', 0.75)
+
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'], 'ddns-send-updates', True)
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-generated-prefix', 'myfunnyprefix')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'ddns-qualifying-suffix', 'suffix.lan')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-set', 'xXyYzZ')
+ self.verify_config_value(obj, ['Dhcp4', 'shared-networks', 0, 'subnet4'], 'hostname-char-replacement', '_xXx_')
- # Verify options
+ # Verify keys and domains configuration in the D2 config
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
- {'name': 'routers', 'data': router})
+ d2_obj,
+ ['DhcpDdns', 'tsig-keys'],
+ {'name': 'domain-lan-updates', 'algorithm': 'HMAC-SHA256', 'secret': 'SXQncyBXZWRuZXNkYXkgbWFoIGR1ZGVzIQ=='}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'tsig-keys'],
+ {'name': 'reverse-0-168-192', 'algorithm': 'HMAC-SHA256', 'secret': 'VGhhbmsgR29kIGl0J3MgRnJpZGF5IQ=='}
+ )
- # Verify pools
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'name', 'domain.lan')
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0], 'key-name', 'domain-lan-updates')
self.verify_config_object(
- obj,
- ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
- {'pool': f'{range_0_start} - {range_0_stop}'})
+ d2_obj,
+ ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '192.168.0.1'}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'forward-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '100.100.0.1'}
+ )
+
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'name', '0.168.192.in-addr.arpa')
+ self.verify_config_value(d2_obj, ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0], 'key-name', 'reverse-0-168-192')
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '192.168.0.1', 'port': 1053}
+ )
+ self.verify_config_object(
+ d2_obj,
+ ['DhcpDdns', 'reverse-ddns', 'ddns-domains', 0, 'dns-servers'],
+ {'ip-address': '100.100.0.1', 'port': 1153}
+ )
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
- self.assertTrue(process_named_running(CTRL_PROCESS_NAME))
+ self.assertTrue(process_named_running(D2_PROCESS_NAME))
def test_dhcp_on_interface_with_vrf(self):
self.cli_set(['interfaces', 'ethernet', 'eth1', 'address', '10.1.1.1/30'])
self.cli_set(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_set(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'protocols', 'static', 'route', '10.1.10.0/24', 'next-hop', '10.1.1.2'])
+ self.cli_set(
+ [
+ 'protocols',
+ 'static',
+ 'route',
+ '10.1.10.0/24',
+ 'interface',
+ 'eth1',
+ 'vrf',
+ 'SMOKE-DHCP',
+ ]
+ )
+ self.cli_set(
+ [
+ 'vrf',
+ 'name',
+ 'SMOKE-DHCP',
+ 'protocols',
+ 'static',
+ 'route',
+ '10.1.10.0/24',
+ 'next-hop',
+ '10.1.1.2',
+ ]
+ )
self.cli_set(['vrf', 'name', 'SMOKE-DHCP', 'table', '1000'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'subnet-id', '1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'default-router', '10.1.10.1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'option', 'name-server', '1.1.1.1'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'start', '10.1.10.10'])
- self.cli_set(base_path + ['shared-network-name', 'SMOKE-DHCP-NETWORK', 'subnet', '10.1.10.0/24', 'range', '1', 'stop', '10.1.10.20'])
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'subnet-id',
+ '1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'option',
+ 'default-router',
+ '10.1.10.1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'option',
+ 'name-server',
+ '1.1.1.1',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'range',
+ '1',
+ 'start',
+ '10.1.10.10',
+ ]
+ )
+ self.cli_set(
+ base_path
+ + [
+ 'shared-network-name',
+ 'SMOKE-DHCP-NETWORK',
+ 'subnet',
+ '10.1.10.0/24',
+ 'range',
+ '1',
+ 'stop',
+ '10.1.10.20',
+ ]
+ )
self.cli_set(base_path + ['listen-address', '10.1.1.1'])
self.cli_commit()
config = read_file(KEA4_CONF)
obj = loads(config)
- self.verify_config_value(obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1'])
+ self.verify_config_value(
+ obj, ['Dhcp4', 'interfaces-config'], 'interfaces', ['eth1/10.1.1.1']
+ )
self.cli_delete(['interfaces', 'ethernet', 'eth1', 'vrf', 'SMOKE-DHCP'])
- self.cli_delete(['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf'])
+ self.cli_delete(
+ ['protocols', 'static', 'route', '10.1.10.0/24', 'interface', 'eth1', 'vrf']
+ )
self.cli_delete(['vrf', 'name', 'SMOKE-DHCP'])
self.cli_commit()
+ def test_dhcp_hostsd_lease_sync(self):
+ shared_net_name = 'SMOKE-LEASE-SYNC'
+ domain_name = 'sync.private'
+
+ client_range = range(1, 4)
+ subnet_range_start = inc_ip(subnet, 10)
+ subnet_range_stop = inc_ip(subnet, 20)
+
+ def internal_cleanup():
+ for seq in client_range:
+ ip_addr = inc_ip(subnet, seq)
+ kea_delete_lease(4, ip_addr)
+ cmd(
+ f'{HOSTSD_CLIENT} --delete-hosts --tag dhcp-server-{ip_addr} --apply'
+ )
+
+ self.addClassCleanup(internal_cleanup)
+
+ pool = base_path + ['shared-network-name', shared_net_name, 'subnet', subnet]
+ self.cli_set(pool + ['subnet-id', '1'])
+ self.cli_set(pool + ['option', 'domain-name', domain_name])
+ self.cli_set(pool + ['range', '0', 'start', subnet_range_start])
+ self.cli_set(pool + ['range', '0', 'stop', subnet_range_stop])
+
+ # 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'], 'id', 1
+ )
+
+ # Verify options
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'option-data'],
+ {'name': 'domain-name', 'data': domain_name},
+ )
+ self.verify_config_object(
+ obj,
+ ['Dhcp4', 'shared-networks', 0, 'subnet4', 0, 'pools'],
+ {'pool': f'{subnet_range_start} - {subnet_range_stop}'},
+ )
+
+ # Check for running process
+ self.verify_service_running()
+
+ # All up and running, now test vyos-hostsd store
+
+ # 1. Inject leases into kea
+ for seq in client_range:
+ client = f'client{seq}'
+ mac = f'00:50:00:00:00:{seq:02}'
+ ip = inc_ip(subnet, seq)
+ kea_add_lease(4, ip, host_name=client, mac_address=mac)
+
+ # 2. Verify that leases are not available in vyos-hostsd
+ tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}')
+ host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}')
+ self.assertFalse(host_json.strip('{}'))
+
+ # 3. Restart the service to trigger vyos-hostsd sync and wait for it to start
+ self.assertTrue(process_named_running(PROCESS_NAME, timeout=30))
+
+ # 4. Verify that leases are synced and available in vyos-hostsd
+ tag_regex = re.escape(f'dhcp-server-{subnet.rsplit(".", 1)[0]}')
+ host_json = cmd(f'{HOSTSD_CLIENT} --get-hosts {tag_regex}')
+ self.assertTrue(host_json)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_dhcpv6-server.py b/smoketest/scripts/cli/test_service_dhcpv6-server.py
index 6ecf6c1cf..6535ca72d 100755
--- a/smoketest/scripts/cli/test_service_dhcpv6-server.py
+++ b/smoketest/scripts/cli/test_service_dhcpv6-server.py
@@ -108,6 +108,7 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
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 + ['option', 'capwap-controller', dns_1])
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])
@@ -157,6 +158,10 @@ class TestServiceDHCPv6Server(VyOSUnitTestSHIM.TestCase):
self.verify_config_object(
obj,
['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
+ {'name': 'capwap-ac-v6', 'data': dns_1})
+ self.verify_config_object(
+ obj,
+ ['Dhcp6', 'shared-networks', 0, 'subnet6', 0, 'option-data'],
{'name': 'dns-servers', 'data': f'{dns_1}, {dns_2}'})
self.verify_config_object(
obj,
diff --git a/smoketest/scripts/cli/test_service_https.py b/smoketest/scripts/cli/test_service_https.py
index 04c4a2e51..b4fe35d81 100755
--- a/smoketest/scripts/cli/test_service_https.py
+++ b/smoketest/scripts/cli/test_service_https.py
@@ -16,6 +16,7 @@
import unittest
import json
+import psutil
from requests import request
from urllib3.exceptions import InsecureRequestWarning
@@ -113,6 +114,29 @@ class TestHTTPSService(VyOSUnitTestSHIM.TestCase):
# Check for stopped process
self.assertFalse(process_named_running(PROCESS_NAME))
+ def test_listen_address(self):
+ test_prefix = ['192.0.2.1/26', '2001:db8:1::ffff/64']
+ test_addr = [ i.split('/')[0] for i in test_prefix ]
+ for i, addr in enumerate(test_prefix):
+ self.cli_set(['interfaces', 'dummy', f'dum{i}', 'address', addr])
+
+ key = 'MySuperSecretVyOS'
+ self.cli_set(base_path + ['api', 'keys', 'id', 'key-01', 'key', key])
+ # commit base config first, for testing update of listen-address
+ self.cli_commit()
+
+ for addr in test_addr:
+ self.cli_set(base_path + ['listen-address', addr])
+ self.cli_commit()
+
+ res = set()
+ t = psutil.net_connections(kind="tcp")
+ for c in t:
+ if c.laddr.port == 443:
+ res.add(c.laddr.ip)
+
+ self.assertEqual(res, set(test_addr))
+
def test_certificate(self):
cert_name = 'test_https'
dh_name = 'dh-test'
diff --git a/smoketest/scripts/cli/test_service_ids_ddos-protection.py b/smoketest/scripts/cli/test_service_ids_ddos-protection.py
deleted file mode 100755
index 91b056eea..000000000
--- a/smoketest/scripts/cli/test_service_ids_ddos-protection.py
+++ /dev/null
@@ -1,116 +0,0 @@
-#!/usr/bin/env python3
-#
-# Copyright (C) 2022 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 unittest
-
-from base_vyostest_shim import VyOSUnitTestSHIM
-
-from vyos.configsession import ConfigSessionError
-from vyos.utils.process import process_named_running
-from vyos.utils.file import read_file
-
-PROCESS_NAME = 'fastnetmon'
-FASTNETMON_CONF = '/run/fastnetmon/fastnetmon.conf'
-NETWORKS_CONF = '/run/fastnetmon/networks_list'
-EXCLUDED_NETWORKS_CONF = '/run/fastnetmon/excluded_networks_list'
-base_path = ['service', 'ids', 'ddos-protection']
-
-class TestServiceIDS(VyOSUnitTestSHIM.TestCase):
- @classmethod
- def setUpClass(cls):
- super(TestServiceIDS, 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):
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
- # delete test config
- self.cli_delete(base_path)
- self.cli_commit()
-
- self.assertFalse(os.path.exists(FASTNETMON_CONF))
- self.assertFalse(process_named_running(PROCESS_NAME))
-
- def test_fastnetmon(self):
- networks = ['10.0.0.0/24', '10.5.5.0/24', '2001:db8:10::/64', '2001:db8:20::/64']
- excluded_networks = ['10.0.0.1/32', '2001:db8:10::1/128']
- interfaces = ['eth0', 'eth1']
- fps = '3500'
- mbps = '300'
- pps = '60000'
-
- self.cli_set(base_path + ['mode', 'mirror'])
- # Required network!
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- for tmp in networks:
- self.cli_set(base_path + ['network', tmp])
-
- # optional excluded-network!
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- for tmp in excluded_networks:
- self.cli_set(base_path + ['excluded-network', tmp])
-
- # Required interface(s)!
- with self.assertRaises(ConfigSessionError):
- self.cli_commit()
- for tmp in interfaces:
- self.cli_set(base_path + ['listen-interface', tmp])
-
- self.cli_set(base_path + ['direction', 'in'])
- self.cli_set(base_path + ['threshold', 'general', 'fps', fps])
- self.cli_set(base_path + ['threshold', 'general', 'pps', pps])
- self.cli_set(base_path + ['threshold', 'general', 'mbps', mbps])
-
- # commit changes
- self.cli_commit()
-
- # Check configured port
- config = read_file(FASTNETMON_CONF)
- self.assertIn(f'mirror_afpacket = on', config)
- self.assertIn(f'process_incoming_traffic = on', config)
- self.assertIn(f'process_outgoing_traffic = off', config)
- self.assertIn(f'ban_for_flows = on', config)
- self.assertIn(f'threshold_flows = {fps}', config)
- self.assertIn(f'ban_for_bandwidth = on', config)
- self.assertIn(f'threshold_mbps = {mbps}', config)
- self.assertIn(f'ban_for_pps = on', config)
- self.assertIn(f'threshold_pps = {pps}', config)
- # default
- self.assertIn(f'enable_ban = on', config)
- self.assertIn(f'enable_ban_ipv6 = on', config)
- self.assertIn(f'ban_time = 1900', config)
-
- tmp = ','.join(interfaces)
- self.assertIn(f'interfaces = {tmp}', config)
-
-
- network_config = read_file(NETWORKS_CONF)
- for tmp in networks:
- self.assertIn(f'{tmp}', network_config)
-
- excluded_network_config = read_file(EXCLUDED_NETWORKS_CONF)
- for tmp in excluded_networks:
- self.assertIn(f'{tmp}', excluded_network_config)
-
-if __name__ == '__main__':
- unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py
index ab0898d17..3b3c205cd 100755
--- a/smoketest/scripts/cli/test_service_ipoe-server.py
+++ b/smoketest/scripts/cli/test_service_ipoe-server.py
@@ -260,7 +260,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
tmp = ','.join(vlans)
self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon'])
- def test_ipoe_server_static_client_ip(self):
+ def test_ipoe_server_static_client_ip_address(self):
mac_address = '08:00:27:2f:d8:06'
ip_address = '192.0.2.100'
@@ -274,7 +274,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
interface,
'mac',
mac_address,
- 'static-ip',
+ 'ip-address',
ip_address,
]
)
@@ -295,6 +295,28 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}"""
tmp = re.findall(regex, tmp)
self.assertTrue(tmp)
+ def test_ipoe_server_start_session(self):
+ start_session = 'auto'
+
+ # Configuration of local authentication for PPPoE server
+ self.basic_config()
+ self.cli_commit()
+
+ # Validate configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+ # if 'start-session' option is not set the default value is 'dhcp'
+ self.assertIn(f'start=dhcpv4', conf['ipoe']['interface'])
+
+ # change 'start-session' option to 'auto'
+ self.set(['interface', interface, 'start-session', start_session])
+ self.cli_commit()
+
+ # Validate changed configuration values
+ conf = ConfigParser(allow_no_value=True, delimiters='=', strict=False)
+ conf.read(self._config_file)
+ self.assertIn(f'start={start_session}', conf['ipoe']['interface'])
+
@unittest.skip("PPP is not a part of IPoE")
def test_accel_ppp_options(self):
pass
diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py
index 9d72ef78f..c73707e0d 100755
--- a/smoketest/scripts/cli/test_service_lldp.py
+++ b/smoketest/scripts/cli/test_service_lldp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2022-2024 VyOS maintainers and contributors
+# Copyright (C) 2022-2025 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
@@ -117,6 +117,8 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
config = read_file(LLDPD_CONF)
self.assertIn(f'configure ports {interface} med location elin "{elin}"', config)
+ # This is the CLI default mode
+ self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config)
self.assertIn(f'configure system interface pattern "{interface}"', config)
def test_06_lldp_snmp(self):
@@ -134,5 +136,50 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase):
self.cli_delete(['service', 'snmp'])
+ def test_07_lldp_interface_mode(self):
+ interfaces = Section.interfaces('ethernet', vlan=False)
+
+ # set interface mode to 'tx'
+ self.cli_set(base_path + ['interface', 'all'])
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'disable'])
+ # commit changes
+ self.cli_commit()
+
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status disable', config)
+
+ # Change configuration to rx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'rx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status rx-only', config)
+
+ # Change configuration to tx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'tx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status tx-only', config)
+
+ # Change configuration to rx-only
+ for interface in interfaces:
+ self.cli_set(base_path + ['interface', interface, 'mode', 'rx-tx'])
+ # commit changes
+ self.cli_commit()
+ # verify configuration
+ config = read_file(LLDPD_CONF)
+ for interface in interfaces:
+ self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py
index 6dbb6add4..33ce93ef4 100755
--- a/smoketest/scripts/cli/test_service_router-advert.py
+++ b/smoketest/scripts/cli/test_service_router-advert.py
@@ -252,6 +252,118 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase):
tmp = get_config_value('AdvIntervalOpt')
self.assertEqual(tmp, 'off')
+ def test_auto_ignore(self):
+ isp_prefix = '2001:db8::/64'
+ ula_prefixes = ['fd00::/64', 'fd01::/64']
+
+ # configure wildcard prefix
+ self.cli_set(base_path + ['prefix', '::/64'])
+
+ # test auto-ignore CLI behaviors with no prefix overrides
+ # set auto-ignore for all three prefixes
+ self.cli_set(base_path + ['auto-ignore', isp_prefix])
+
+ for ula_prefix in ula_prefixes:
+ self.cli_set(base_path + ['auto-ignore', ula_prefix])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # ensure autoignoreprefixes block is generated in config file
+ tmp = f'autoignoreprefixes' + ' {'
+ self.assertIn(tmp, config)
+
+ # ensure all three prefixes are contained in the block
+ self.assertIn(f' {isp_prefix};', config)
+ for ula_prefix in ula_prefixes:
+ self.assertIn(f' {ula_prefix};', config)
+
+ # remove a prefix and verify it's gone
+ self.cli_delete(base_path + ['auto-ignore', ula_prefixes[1]])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ self.assertNotIn(f' {ula_prefixes[1]};', config)
+
+ # ensure remaining two prefixes are still present
+ self.assertIn(f' {ula_prefixes[0]};', config)
+ self.assertIn(f' {isp_prefix};', config)
+
+ # remove the remaining two prefixes and verify the config block is gone
+ self.cli_delete(base_path + ['auto-ignore', ula_prefixes[0]])
+ self.cli_delete(base_path + ['auto-ignore', isp_prefix])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ tmp = f'autoignoreprefixes' + ' {'
+ self.assertNotIn(tmp, config)
+
+ # test wildcard prefix overrides, with and without auto-ignore CLI configuration
+ newline = '\n'
+ left_curly = '{'
+ right_curly = '}'
+
+ # override ULA prefixes
+ for ula_prefix in ula_prefixes:
+ self.cli_set(base_path + ['prefix', ula_prefix])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # ensure autoignoreprefixes block is generated in config file with both prefixes
+ tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};'
+ self.assertIn(tmp, config)
+
+ # remove a ULA prefix and ensure there is only one prefix in the config block
+ self.cli_delete(base_path + ['prefix', ula_prefixes[0]])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # ensure autoignoreprefixes block is generated in config file with only one prefix
+ tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[1]};{newline} {right_curly};'
+ self.assertIn(tmp, config)
+
+ # exclude a prefix with auto-ignore CLI syntax
+ self.cli_set(base_path + ['auto-ignore', ula_prefixes[0]])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # verify that both prefixes appear in config block once again
+ tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};'
+ self.assertIn(tmp, config)
+
+ # override first ULA prefix again
+ # first ULA is auto-ignored in CLI, it must appear only once in config
+ self.cli_set(base_path + ['prefix', ula_prefixes[0]])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # verify that both prefixes appear uniquely
+ tmp = f'autoignoreprefixes' + f' {left_curly}{newline} {ula_prefixes[0]};{newline} {ula_prefixes[1]};{newline} {right_curly};'
+ self.assertIn(tmp, config)
+
+ # remove wildcard prefix and verify config block is gone
+ self.cli_delete(base_path + ['prefix', '::/64'])
+
+ # commit and reload config
+ self.cli_commit()
+ config = read_file(RADVD_CONF)
+
+ # verify config block is gone
+ tmp = f'autoignoreprefixes' + ' {'
+ self.assertNotIn(tmp, config)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_service_webproxy.py b/smoketest/scripts/cli/test_service_webproxy.py
index 2b3f6d21c..ab4707a61 100755
--- a/smoketest/scripts/cli/test_service_webproxy.py
+++ b/smoketest/scripts/cli/test_service_webproxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2020-2022 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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
@@ -297,6 +297,22 @@ class TestServiceWebProxy(VyOSUnitTestSHIM.TestCase):
# Check for running process
self.assertTrue(process_named_running(PROCESS_NAME))
+ def test_06_nocache_domain_proxy(self):
+ domains_nocache = ['test1.net', 'test2.net']
+ self.cli_set(base_path + ['listen-address', listen_ip])
+ for domain in domains_nocache:
+ self.cli_set(base_path + ['domain-noncache', domain])
+ # commit changes
+ self.cli_commit()
+
+ config = read_file(PROXY_CONF)
+
+ for domain in domains_nocache:
+ self.assertIn(f'acl NOCACHE dstdomain {domain}', config)
+ self.assertIn(f'no_cache deny NOCACHE', config)
+
+ # Check for running process
+ self.assertTrue(process_named_running(PROCESS_NAME))
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_login.py b/smoketest/scripts/cli/test_system_login.py
index d79f5521c..71dec68d8 100755
--- a/smoketest/scripts/cli/test_system_login.py
+++ b/smoketest/scripts/cli/test_system_login.py
@@ -42,6 +42,7 @@ from vyos.xml_ref import default_value
base_path = ['system', 'login']
users = ['vyos1', 'vyos-roxx123', 'VyOS-123_super.Nice']
+weak_passwd_user = ['test_user', 'passWord1']
ssh_test_command = '/opt/vyatta/bin/vyatta-op-cmd-wrapper show version'
@@ -194,18 +195,20 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
def test_system_login_user(self):
for user in users:
name = f'VyOS Roxx {user}'
+ passwd = f'{user}-pSWd-t3st'
home_dir = f'/tmp/smoketest/{user}'
- self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', user])
+ self.cli_set(base_path + ['user', user, 'authentication', 'plaintext-password', passwd])
self.cli_set(base_path + ['user', user, 'full-name', name])
self.cli_set(base_path + ['user', user, 'home-directory', home_dir])
self.cli_commit()
for user in users:
+ passwd = f'{user}-pSWd-t3st'
tmp = ['su','-', user]
proc = Popen(tmp, stdin=PIPE, stdout=PIPE, stderr=PIPE)
- tmp = f'{user}\nuname -a'
+ tmp = f'{passwd}\nuname -a'
proc.stdin.write(tmp.encode())
proc.stdin.flush()
(stdout, stderr) = proc.communicate()
@@ -229,6 +232,17 @@ class TestSystemLogin(VyOSUnitTestSHIM.TestCase):
tmp = cmd(f'sudo passwd -S {locked_user}')
self.assertIn(f'{locked_user} P ', tmp)
+ def test_system_login_weak_password_warning(self):
+ self.cli_set(base_path + [
+ 'user', weak_passwd_user[0], 'authentication',
+ 'plaintext-password', weak_passwd_user[1]
+ ])
+
+ out = self.cli_commit().strip()
+
+ self.assertIn('WARNING: The password complexity is too low', out)
+ self.cli_delete(base_path + ['user', weak_passwd_user[0]])
+
def test_system_login_otp(self):
otp_user = 'otp-test_user'
otp_password = 'SuperTestPassword'
diff --git a/smoketest/scripts/cli/test_system_option.py b/smoketest/scripts/cli/test_system_option.py
index f3112cf0b..c7f8c1f3e 100755
--- a/smoketest/scripts/cli/test_system_option.py
+++ b/smoketest/scripts/cli/test_system_option.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright (C) 2024-2025 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
@@ -16,14 +16,18 @@
import os
import unittest
+
from base_vyostest_shim import VyOSUnitTestSHIM
+
+from vyos.configsession import ConfigSessionError
+from vyos.utils.cpu import get_cpus
from vyos.utils.file import read_file
from vyos.utils.process import is_systemd_service_active
from vyos.utils.system import sysctl_read
+from vyos.system import image
base_path = ['system', 'option']
-
class TestSystemOption(VyOSUnitTestSHIM.TestCase):
def tearDown(self):
self.cli_delete(base_path)
@@ -96,6 +100,60 @@ class TestSystemOption(VyOSUnitTestSHIM.TestCase):
self.cli_commit()
self.assertFalse(os.path.exists(ssh_client_opt_file))
+ def test_kernel_options(self):
+ amd_pstate_mode = 'active'
+ isolate_cpus = '1,2,3'
+ nohz_full = '2'
+ rcu_no_cbs = '1,2,4-5'
+ default_hp_size = '2M'
+ hp_size_1g = '1G'
+ hp_size_2m = '2M'
+ hp_count_1g = '2'
+ hp_count_2m = '512'
+
+ self.cli_set(['system', 'option', 'kernel', 'cpu', 'disable-nmi-watchdog'])
+ self.cli_set(['system', 'option', 'kernel', 'cpu', 'isolate-cpus', isolate_cpus])
+ self.cli_set(['system', 'option', 'kernel', 'cpu', 'nohz-full', nohz_full])
+ self.cli_set(['system', 'option', 'kernel', 'cpu', 'rcu-no-cbs', rcu_no_cbs])
+ self.cli_set(['system', 'option', 'kernel', 'disable-hpet'])
+ self.cli_set(['system', 'option', 'kernel', 'disable-mce'])
+ self.cli_set(['system', 'option', 'kernel', 'disable-mitigations'])
+ self.cli_set(['system', 'option', 'kernel', 'disable-power-saving'])
+ self.cli_set(['system', 'option', 'kernel', 'disable-softlockup'])
+ self.cli_set(['system', 'option', 'kernel', 'memory', 'disable-numa-balancing'])
+ self.cli_set(['system', 'option', 'kernel', 'memory', 'default-hugepage-size', default_hp_size])
+ self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_1g, 'hugepage-count', hp_count_1g])
+ self.cli_set(['system', 'option', 'kernel', 'memory', 'hugepage-size', hp_size_2m, 'hugepage-count', hp_count_2m])
+ self.cli_set(['system', 'option', 'kernel', 'quiet'])
+
+ self.cli_set(['system', 'option', 'kernel', 'amd-pstate-driver', amd_pstate_mode])
+ cpu_vendor = get_cpus()[0]['vendor_id']
+ if cpu_vendor != 'AuthenticAMD':
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_delete(['system', 'option', 'kernel', 'amd-pstate-driver'])
+
+ self.cli_commit()
+
+ # Read GRUB config file for current running image
+ tmp = read_file(f'{image.grub.GRUB_DIR_VYOS_VERS}/{image.get_running_image()}.cfg')
+ self.assertIn(' mitigations=off', tmp)
+ self.assertIn(' intel_idle.max_cstate=0 processor.max_cstate=1', tmp)
+ self.assertIn(' quiet', tmp)
+ self.assertIn(' nmi_watchdog=0', tmp)
+ self.assertIn(' hpet=disable', tmp)
+ self.assertIn(' mce=off', tmp)
+ self.assertIn(' nosoftlockup', tmp)
+ self.assertIn(f' isolcpus={isolate_cpus}', tmp)
+ self.assertIn(f' nohz_full={nohz_full}', tmp)
+ self.assertIn(f' rcu_nocbs={rcu_no_cbs}', tmp)
+ self.assertIn(f' default_hugepagesz={default_hp_size}', tmp)
+ self.assertIn(f' hugepagesz={hp_size_1g} hugepages={hp_count_1g}', tmp)
+ self.assertIn(f' hugepagesz={hp_size_2m} hugepages={hp_count_2m}', tmp)
+ self.assertIn(' numa_balancing=disable', tmp)
+
+ if cpu_vendor == 'AuthenticAMD':
+ self.assertIn(f' initcall_blacklist=acpi_cpufreq_init amd_pstate={amd_pstate_mode}', tmp)
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/smoketest/scripts/cli/test_system_syslog.py b/smoketest/scripts/cli/test_system_syslog.py
index a86711119..f3e1f65ea 100755
--- a/smoketest/scripts/cli/test_system_syslog.py
+++ b/smoketest/scripts/cli/test_system_syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2019-2024 VyOS maintainers and contributors
+# Copyright (C) 2019-2025 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,24 +14,33 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import re
+import os
import unittest
from base_vyostest_shim import VyOSUnitTestSHIM
+from vyos.configsession import ConfigSessionError
from vyos.utils.file import read_file
+from vyos.utils.process import cmd
from vyos.utils.process import process_named_running
from vyos.xml_ref import default_value
PROCESS_NAME = 'rsyslogd'
-RSYSLOG_CONF = '/etc/rsyslog.d/00-vyos.conf'
+RSYSLOG_CONF = '/run/rsyslog/rsyslog.conf'
base_path = ['system', 'syslog']
-def get_config_value(key):
- tmp = read_file(RSYSLOG_CONF)
- tmp = re.findall(r'\n?{}\s+(.*)'.format(key), tmp)
- return tmp[0]
+dummy_interface = 'dum372874'
+
+def get_config(string=''):
+ """
+ Retrieve current "running configuration" from FRR
+ string: search for a specific start string in the configuration
+ """
+ command = 'cat /run/rsyslog/rsyslog.conf'
+ if string:
+ command += f' | sed -n "/^{string}$/,/}}/p"' # }} required to escape } in f-string
+ return cmd(command)
class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
@classmethod
@@ -41,6 +50,7 @@ class TestRSYSLOGService(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, ['vrf'])
def tearDown(self):
# Check for running process
@@ -50,82 +60,249 @@ class TestRSYSLOGService(VyOSUnitTestSHIM.TestCase):
self.cli_delete(base_path)
self.cli_commit()
+ # The default syslog implementation should make syslog.service a
+ # symlink to itself
+ self.assertEqual(os.readlink('/etc/systemd/system/syslog.service'),
+ '/lib/systemd/system/rsyslog.service')
+
# Check for running process
self.assertFalse(process_named_running(PROCESS_NAME))
- def test_syslog_basic(self):
- host1 = '127.0.0.10'
- host2 = '127.0.0.20'
-
- self.cli_set(base_path + ['host', host1, 'port', '999'])
- self.cli_set(base_path + ['host', host1, 'facility', 'all', 'level', 'all'])
- self.cli_set(base_path + ['host', host2, 'facility', 'kern', 'level', 'err'])
- self.cli_set(base_path + ['console', 'facility', 'all', 'level', 'warning'])
-
+ def test_console(self):
+ level = 'warning'
+ self.cli_set(base_path + ['console', 'facility', 'all', 'level'], value=level)
self.cli_commit()
- # verify log level and facilities in config file
- # *.warning /dev/console
- # *.* @198.51.100.1:999
- # kern.err @192.0.2.1:514
+
+ rsyslog_conf = get_config()
config = [
- get_config_value('\*.\*'),
- get_config_value('kern.err'),
- get_config_value('\*.warning'),
+ f'if prifilt("*.{level}") then {{', # {{ required to escape { in f-string
+ 'action(type="omfile" file="/dev/console")',
]
- expected = [f'@{host1}:999', f'@{host2}:514', '/dev/console']
+ for tmp in config:
+ self.assertIn(tmp, rsyslog_conf)
- for i in range(0, 3):
- self.assertIn(expected[i], config[i])
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
-
- def test_syslog_global(self):
+ def test_basic(self):
hostname = 'vyos123'
- domainname = 'example.local'
- self.cli_set(['system', 'host-name', hostname])
- self.cli_set(['system', 'domain-name', domainname])
- self.cli_set(base_path + ['global', 'marker', 'interval', '600'])
- self.cli_set(base_path + ['global', 'preserve-fqdn'])
- self.cli_set(base_path + ['global', 'facility', 'kern', 'level', 'err'])
+ domain_name = 'example.local'
+ default_marker_interval = default_value(base_path + ['marker', 'interval'])
+
+ facility = {
+ 'auth': {'level': 'info'},
+ 'kern': {'level': 'debug'},
+ 'all': {'level': 'notice'},
+ }
+
+ self.cli_set(['system', 'host-name'], value=hostname)
+ self.cli_set(['system', 'domain-name'], value=domain_name)
+ self.cli_set(base_path + ['preserve-fqdn'])
+
+ for tmp, tmp_options in facility.items():
+ level = tmp_options['level']
+ self.cli_set(base_path + ['local', 'facility', tmp, 'level'], value=level)
self.cli_commit()
- config = read_file(RSYSLOG_CONF)
+ config = get_config('')
expected = [
- '$MarkMessagePeriod 600',
- '$PreserveFQDN on',
- 'kern.err',
- f'$LocalHostName {hostname}.{domainname}',
+ f'module(load="immark" interval="{default_marker_interval}")',
+ 'global(preserveFQDN="on")',
+ f'global(localHostname="{hostname}.{domain_name}")',
]
-
for e in expected:
self.assertIn(e, config)
- # Check for running process
- self.assertTrue(process_named_running(PROCESS_NAME))
- def test_syslog_remote(self):
- rhost = '169.254.0.1'
- default_port = default_value(base_path + ['host', rhost, 'port'])
+ config = get_config('#### GLOBAL LOGGING ####')
+ prifilt = []
+ for tmp, tmp_options in facility.items():
+ if tmp == 'all':
+ tmp = '*'
+ level = tmp_options['level']
+ prifilt.append(f'{tmp}.{level}')
+
+ prifilt.sort()
+ prifilt = ','.join(prifilt)
- self.cli_set(base_path + ['global', 'facility', 'all', 'level', 'info'])
- self.cli_set(base_path + ['global', 'facility', 'local7', 'level', 'debug'])
- self.cli_set(base_path + ['host', rhost, 'facility', 'all', 'level', 'all'])
- self.cli_set(base_path + ['host', rhost, 'protocol', 'tcp'])
+ self.assertIn(f'if prifilt("{prifilt}") then {{', config)
+ self.assertIn( ' action(', config)
+ self.assertIn( ' type="omfile"', config)
+ self.assertIn( ' file="/var/log/messages"', config)
+ self.assertIn( ' rotation.sizeLimit="524288"', config)
+ self.assertIn( ' rotation.sizeLimitCommand="/usr/sbin/logrotate /etc/logrotate.d/vyos-rsyslog"', config)
+ self.cli_set(base_path + ['marker', 'disable'])
self.cli_commit()
- config = read_file(RSYSLOG_CONF)
- self.assertIn(f'*.* @@{rhost}:{default_port}', config)
+ config = get_config('')
+ self.assertNotIn('module(load="immark"', config)
+
+ def test_remote(self):
+ dummy_if_path = ['interfaces', 'dummy', dummy_interface]
+ rhosts = {
+ '169.254.0.1': {
+ 'facility': {'auth' : {'level': 'info'}},
+ 'protocol': 'udp',
+ },
+ '2001:db8::1': {
+ 'facility': {'all' : {'level': 'debug'}},
+ 'port': '1514',
+ 'protocol': 'udp',
+ },
+ 'syslog.vyos.net': {
+ 'facility': {'all' : {'level': 'debug'}},
+ 'port': '1515',
+ 'protocol': 'tcp',
+ },
+ '169.254.0.3': {
+ 'facility': {'auth' : {'level': 'info'},
+ 'kern' : {'level': 'debug'},
+ 'all' : {'level': 'notice'},
+ },
+ 'format': ['include-timezone', 'octet-counted'],
+ 'protocol': 'tcp',
+ 'port': '10514',
+ },
+ }
+ default_port = default_value(base_path + ['remote', next(iter(rhosts)), 'port'])
+ default_protocol = default_value(base_path + ['remote', next(iter(rhosts)), 'protocol'])
+
+ for remote, remote_options in rhosts.items():
+ remote_base = base_path + ['remote', remote]
+
+ if 'port' in remote_options:
+ self.cli_set(remote_base + ['port'], value=remote_options['port'])
+
+ if 'facility' in remote_options:
+ for facility, facility_options in remote_options['facility'].items():
+ level = facility_options['level']
+ self.cli_set(remote_base + ['facility', facility, 'level'],
+ value=level)
+
+ if 'format' in remote_options:
+ for format in remote_options['format']:
+ self.cli_set(remote_base + ['format'], value=format)
+
+ if 'protocol' in remote_options:
+ protocol = remote_options['protocol']
+ self.cli_set(remote_base + ['protocol'], value=protocol)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.cli_set(remote_base + ['source-address', source_address])
+
+ # check validate() - source address does not exist
+ with self.assertRaises(ConfigSessionError):
+ self.cli_commit()
+ self.cli_set(dummy_if_path + ['address', f'{source_address}/32'])
- # Change default port and enable "octet-counting" mode
- port = '10514'
- self.cli_set(base_path + ['host', rhost, 'port', port])
- self.cli_set(base_path + ['host', rhost, 'format', 'octet-counted'])
self.cli_commit()
config = read_file(RSYSLOG_CONF)
- self.assertIn(f'*.* @@(o){rhost}:{port}', config)
+ for remote, remote_options in rhosts.items():
+ config = get_config(f'# Remote syslog to {remote}')
+ prifilt = []
+ if 'facility' in remote_options:
+ for facility, facility_options in remote_options['facility'].items():
+ level = facility_options['level']
+ if facility == 'all':
+ facility = '*'
+ prifilt.append(f'{facility}.{level}')
+
+ prifilt.sort()
+ prifilt = ','.join(prifilt)
+ if not prifilt:
+ # Skip test - as we do not render anything if no facility is set
+ continue
+
+ self.assertIn(f'if prifilt("{prifilt}") then {{', config)
+ self.assertIn( ' type="omfwd"', config)
+ self.assertIn(f' target="{remote}"', config)
+
+ port = default_port
+ if 'port' in remote_options:
+ port = remote_options['port']
+ self.assertIn(f'port="{port}"', config)
+
+ protocol = default_protocol
+ if 'protocol' in remote_options:
+ protocol = remote_options['protocol']
+ self.assertIn(f'protocol="{protocol}"', config)
+
+ if 'format' in remote_options:
+ if 'include-timezone' in remote_options['format']:
+ self.assertIn( ' template="RSYSLOG_SyslogProtocol23Format"', config)
+
+ if 'octet-counted' in remote_options['format']:
+ self.assertIn( ' TCP_Framing="octet-counted"', config)
+ else:
+ self.assertIn( ' TCP_Framing="traditional"', config)
+
+ # cleanup dummy interface
+ self.cli_delete(dummy_if_path)
+
+ def test_vrf_source_address(self):
+ rhosts = {
+ '169.254.0.10': { },
+ '169.254.0.11': {
+ 'vrf': {'name' : 'red', 'table' : '12321'},
+ 'source_address' : '169.254.0.11',
+ },
+ '169.254.0.12': {
+ 'vrf': {'name' : 'green', 'table' : '12322'},
+ 'source_address' : '169.254.0.12',
+ },
+ '169.254.0.13': {
+ 'vrf': {'name' : 'blue', 'table' : '12323'},
+ 'source_address' : '169.254.0.13',
+ },
+ }
+
+ for remote, remote_options in rhosts.items():
+ remote_base = base_path + ['remote', remote]
+ self.cli_set(remote_base + ['facility', 'all'])
+
+ vrf = None
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.cli_set(['vrf', 'name', vrf, 'table'],
+ value=remote_options['vrf']['table'])
+ self.cli_set(remote_base + ['vrf'], value=vrf)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.cli_set(remote_base + ['source-address'],
+ value=source_address)
+
+ idx = source_address.split('.')[-1]
+ self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'address'],
+ value=f'{source_address}/32')
+ if vrf:
+ self.cli_set(['interfaces', 'dummy', f'dum{idx}', 'vrf'],
+ value=vrf)
+
+ self.cli_commit()
+
+ for remote, remote_options in rhosts.items():
+ config = get_config(f'# Remote syslog to {remote}')
+
+ self.assertIn(f'target="{remote}"', config)
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.assertIn(f'Device="{vrf}"', config)
+
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ self.assertIn(f'Address="{source_address}"', config)
+
+ # Cleanup VRF/Dummy interfaces
+ for remote, remote_options in rhosts.items():
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']['name']
+ self.cli_delete(['vrf', 'name', vrf])
+ if 'source_address' in remote_options:
+ source_address = remote_options['source_address']
+ idx = source_address.split('.')[-1]
+ self.cli_delete(['interfaces', 'dummy', f'dum{idx}'])
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 91a76e6f6..c1d943bde 100755
--- a/smoketest/scripts/cli/test_vpn_ipsec.py
+++ b/smoketest/scripts/cli/test_vpn_ipsec.py
@@ -352,6 +352,94 @@ class TestVPNIPsec(VyOSUnitTestSHIM.TestCase):
self.tearDownPKI()
+ def test_site_to_site_vti_ts_afi(self):
+ local_address = '192.0.2.10'
+ vti = 'vti10'
+ # IKE
+ self.cli_set(base_path + ['ike-group', ike_group, 'key-exchange', 'ikev2'])
+ self.cli_set(base_path + ['ike-group', ike_group, 'disable-mobike'])
+ # ESP
+ self.cli_set(base_path + ['esp-group', esp_group, 'compression'])
+ # VTI interface
+ self.cli_set(vti_path + [vti, 'address', '10.1.1.1/24'])
+
+ # vpn ipsec auth psk <tag> id <x.x.x.x>
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', local_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', remote_id])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'id', peer_ip])
+ self.cli_set(base_path + ['authentication', 'psk', connection_name, 'secret', secret])
+
+ # Site to site
+ peer_base_path = base_path + ['site-to-site', 'peer', connection_name]
+ self.cli_set(peer_base_path + ['authentication', 'mode', 'pre-shared-secret'])
+ self.cli_set(peer_base_path + ['connection-type', 'none'])
+ self.cli_set(peer_base_path + ['force-udp-encapsulation'])
+ self.cli_set(peer_base_path + ['ike-group', ike_group])
+ self.cli_set(peer_base_path + ['default-esp-group', esp_group])
+ self.cli_set(peer_base_path + ['local-address', local_address])
+ self.cli_set(peer_base_path + ['remote-address', peer_ip])
+ self.cli_set(peer_base_path + ['vti', 'bind', vti])
+ self.cli_set(peer_base_path + ['vti', 'esp-group', esp_group])
+ self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '0.0.0.0/0'])
+ self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.1/32'])
+ self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '192.0.2.3/32'])
+
+ self.cli_commit()
+
+ swanctl_conf = read_file(swanctl_file)
+ if_id = vti.lstrip('vti')
+ # The key defaults to 0 and will match any policies which similarly do
+ # not have a lookup key configuration - thus we shift the key by one
+ # to also support a vti0 interface
+ if_id = str(int(if_id) +1)
+ swanctl_conf_lines = [
+ f'version = 2',
+ f'auth = psk',
+ f'proposals = aes128-sha1-modp1024',
+ f'esp_proposals = aes128-sha1-modp1024',
+ f'local_addrs = {local_address} # dhcp:no',
+ f'mobike = no',
+ f'remote_addrs = {peer_ip}',
+ f'mode = tunnel',
+ f'local_ts = 0.0.0.0/0',
+ f'remote_ts = 192.0.2.1/32,192.0.2.3/32',
+ f'ipcomp = yes',
+ f'start_action = none',
+ f'replay_window = 32',
+ f'if_id_in = {if_id}', # will be 11 for vti10 - shifted by one
+ f'if_id_out = {if_id}',
+ f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
+ ]
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check IPv6 TS
+ self.cli_delete(peer_base_path + ['vti', 'traffic-selector'])
+ self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'local', 'prefix', '::/0'])
+ self.cli_set(peer_base_path + ['vti', 'traffic-selector', 'remote', 'prefix', '::/0'])
+ self.cli_commit()
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ f'local_ts = ::/0',
+ f'remote_ts = ::/0',
+ f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
+ ]
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+ # Check both TS (IPv4 + IPv6)
+ self.cli_delete(peer_base_path + ['vti', 'traffic-selector'])
+ self.cli_commit()
+ swanctl_conf = read_file(swanctl_file)
+ swanctl_conf_lines = [
+ f'local_ts = 0.0.0.0/0,::/0',
+ f'remote_ts = 0.0.0.0/0,::/0',
+ f'updown = "/etc/ipsec.d/vti-up-down {vti}"'
+ ]
+ for line in swanctl_conf_lines:
+ self.assertIn(line, swanctl_conf)
+
+
def test_dmvpn(self):
ike_lifetime = '3600'
esp_lifetime = '1800'
diff --git a/smoketest/scripts/system/test_kernel_options.py b/smoketest/scripts/system/test_kernel_options.py
index b51b0be1d..84e9c145d 100755
--- a/smoketest/scripts/system/test_kernel_options.py
+++ b/smoketest/scripts/system/test_kernel_options.py
@@ -134,5 +134,14 @@ class TestKernelModules(unittest.TestCase):
tmp = re.findall(f'{option}=y', self._config_data)
self.assertTrue(tmp)
+ def test_amd_pstate(self):
+ # AMD pstate driver required as we have "set system option kernel amd-pstate-driver"
+ for option in ['CONFIG_X86_AMD_PSTATE']:
+ tmp = re.findall(f'{option}=y', self._config_data)
+ self.assertTrue(tmp)
+ for option in ['CONFIG_X86_AMD_PSTATE_DEFAULT_MODE']:
+ tmp = re.findall(f'{option}=3', self._config_data)
+ self.assertTrue(tmp)
+
if __name__ == '__main__':
unittest.main(verbosity=2)
diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py
index 594de3eb0..94882fc14 100755
--- a/src/conf_mode/container.py
+++ b/src/conf_mode/container.py
@@ -22,6 +22,7 @@ from ipaddress import ip_address
from ipaddress import ip_network
from json import dumps as json_write
+import psutil
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
@@ -223,6 +224,21 @@ def verify(container):
if not os.path.exists(source):
raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!')
+ if 'tmpfs' in container_config:
+ for tmpfs, tmpfs_config in container_config['tmpfs'].items():
+ if 'destination' not in tmpfs_config:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!')
+ if 'size' in tmpfs_config:
+ free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024
+ if int(tmpfs_config['size']) > free_mem_mb:
+ Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!')
+
+ total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2
+ if int(tmpfs_config['size']) > total_mem_mb:
+ raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!')
+ else:
+ raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!')
+
if 'port' in container_config:
for tmp in container_config['port']:
if not {'source', 'destination'} <= set(container_config['port'][tmp]):
@@ -273,6 +289,13 @@ def verify(container):
if 'registry' in container:
for registry, registry_config in container['registry'].items():
+ if 'mirror' in registry_config:
+ if 'host_name' in registry_config['mirror'] and 'address' in registry_config['mirror']:
+ raise ConfigError(f'Container registry mirror address/host-name are mutually exclusive!')
+
+ if 'path' in registry_config['mirror'] and not registry_config['mirror']['path'].startswith('/'):
+ raise ConfigError('Container registry mirror path must start with "/"!')
+
if 'authentication' not in registry_config:
continue
if not {'username', 'password'} <= set(registry_config['authentication']):
@@ -301,6 +324,11 @@ def generate_run_arguments(name, container_config):
cap = cap.upper().replace('-', '_')
capabilities += f' --cap-add={cap}'
+ # Grant root capabilities to the container
+ privileged = ''
+ if 'privileged' in container_config:
+ privileged = '--privileged'
+
# Add a host device to the container /dev/x:/dev/x
device = ''
if 'device' in container_config:
@@ -362,6 +390,14 @@ def generate_run_arguments(name, container_config):
prop = vol_config['propagation']
volume += f' --volume {svol}:{dvol}:{mode},{prop}'
+ # Mount tmpfs
+ tmpfs = ''
+ if 'tmpfs' in container_config:
+ for tmpfs_config in container_config['tmpfs'].values():
+ dest = tmpfs_config['destination']
+ size = tmpfs_config['size']
+ tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}'
+
host_pid = ''
if 'allow_host_pid' in container_config:
host_pid = '--pid host'
@@ -371,9 +407,9 @@ def generate_run_arguments(name, container_config):
for ns in container_config['name_server']:
name_server += f'--dns {ns}'
- container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \
+ container_base_cmd = f'--detach --interactive --tty --replace {capabilities} {privileged} --cpus {cpu_quota} {sysctl_opt} ' \
f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \
- f'--name {name} {hostname} {device} {port} {name_server} {volume} {env_opt} {label} {uid} {host_pid}'
+ f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}'
entrypoint = ''
if 'entrypoint' in container_config:
diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py
index 768bb127d..274ca2ce6 100755
--- a/src/conf_mode/firewall.py
+++ b/src/conf_mode/firewall.py
@@ -44,6 +44,7 @@ airbag.enable()
nftables_conf = '/run/nftables.conf'
domain_resolver_usage = '/run/use-vyos-domain-resolver-firewall'
+firewall_config_dir = "/config/firewall"
sysctl_file = r'/run/sysctl/10-vyos-firewall.conf'
@@ -53,7 +54,8 @@ valid_groups = [
'network_group',
'port_group',
'interface_group',
- ## Added for group ussage in bridge firewall
+ 'remote_group',
+ ## Added for group usage in bridge firewall
'ipv4_address_group',
'ipv6_address_group',
'ipv4_network_group',
@@ -203,7 +205,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if 'jump' not in rule_conf['action']:
raise ConfigError('jump-target defined, but action jump needed and it is not defined')
target = rule_conf['jump_target']
- if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
+ if hook != 'name': # This is a bit clumsy, but consolidates a chunk of code.
verify_jump_target(firewall, hook, target, family, recursive=True)
else:
verify_jump_target(firewall, hook, target, family, recursive=False)
@@ -266,12 +268,12 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if dict_search_args(rule_conf, 'gre', 'flags', 'checksum') is None:
# There is no builtin match in nftables for the GRE key, so we need to do a raw lookup.
- # The offset of the key within the packet shifts depending on the C-flag.
- # 99% of the time, nobody will have checksums enabled - it's usually a manual config option.
- # We can either assume it is unset unless otherwise directed
+ # The offset of the key within the packet shifts depending on the C-flag.
+ # 99% of the time, nobody will have checksums enabled - it's usually a manual config option.
+ # We can either assume it is unset unless otherwise directed
# (confusing, requires doco to explain why it doesn't work sometimes)
- # or, demand an explicit selection to be made for this specific match rule.
- # This check enforces the latter. The user is free to create rules for both cases.
+ # or, demand an explicit selection to be made for this specific match rule.
+ # This check enforces the latter. The user is free to create rules for both cases.
raise ConfigError('Matching GRE tunnel key requires an explicit checksum flag match. For most cases, use "gre flags checksum unset"')
if dict_search_args(rule_conf, 'gre', 'flags', 'key', 'unset') is not None:
@@ -284,7 +286,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
if gre_inner_value < 0 or gre_inner_value > 65535:
raise ConfigError('inner-proto outside valid ethertype range 0-65535')
except ValueError:
- pass # Symbolic constant, pre-validated before reaching here.
+ pass # Symbolic constant, pre-validated before reaching here.
tcp_flags = dict_search_args(rule_conf, 'tcp', 'flags')
if tcp_flags:
@@ -311,8 +313,8 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
raise ConfigError('Only one of address, fqdn or geoip can be specified')
if 'group' in side_conf:
- if len({'address_group', 'network_group', 'domain_group'} & set(side_conf['group'])) > 1:
- raise ConfigError('Only one address-group, network-group or domain-group can be specified')
+ if len({'address_group', 'network_group', 'domain_group', 'remote_group'} & set(side_conf['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group, remote-group or domain-group can be specified')
for group in valid_groups:
if group in side_conf['group']:
@@ -332,7 +334,7 @@ def verify_rule(firewall, family, hook, priority, rule_id, rule_conf):
error_group = fw_group.replace("_", "-")
- if group in ['address_group', 'network_group', 'domain_group']:
+ if group in ['address_group', 'network_group', 'domain_group', 'remote_group']:
types = [t for t in ['address', 'fqdn', 'geoip'] if t in side_conf]
if types:
raise ConfigError(f'{error_group} and {types[0]} cannot both be defined')
@@ -435,6 +437,16 @@ def verify(firewall):
for ifname in interfaces:
verify_hardware_offload(ifname)
+ if 'offload' in firewall.get('global_options', {}).get('state_policy', {}):
+ offload_path = firewall['global_options']['state_policy']['offload']
+ if 'offload_target' not in offload_path:
+ raise ConfigError('offload-target must be specified')
+
+ offload_target = offload_path['offload_target']
+
+ if not dict_search_args(firewall, 'flowtable', offload_target):
+ raise ConfigError(f'Invalid offload-target. Flowtable "{offload_target}" does not exist on the system')
+
if 'group' in firewall:
for group_type in nested_group_types:
if group_type in firewall['group']:
@@ -442,6 +454,11 @@ def verify(firewall):
for group_name, group in groups.items():
verify_nested_group(group_name, group, groups, [])
+ if 'remote_group' in firewall['group']:
+ for group_name, group in firewall['group']['remote_group'].items():
+ if 'url' not in group:
+ raise ConfigError(f'remote-group {group_name} must have a url configured')
+
for family in ['ipv4', 'ipv6', 'bridge']:
if family in firewall:
for chain in ['name','forward','input','output', 'prerouting']:
@@ -539,6 +556,15 @@ def verify(firewall):
def generate(firewall):
render(nftables_conf, 'firewall/nftables.j2', firewall)
render(sysctl_file, 'firewall/sysctl-firewall.conf.j2', firewall)
+
+ # Cleanup remote-group cache files
+ if os.path.exists(firewall_config_dir):
+ for fw_file in os.listdir(firewall_config_dir):
+ # Delete matching files in 'config/firewall' that no longer exist as a remote-group in config
+ if fw_file.startswith("R_") and fw_file.endswith(".txt"):
+ if 'group' not in firewall or 'remote_group' not in firewall['group'] or fw_file[2:-4] not in firewall['group']['remote_group'].keys():
+ os.unlink(os.path.join(firewall_config_dir, fw_file))
+
return None
def parse_firewall_error(output):
@@ -598,7 +624,7 @@ def apply(firewall):
## DOMAIN RESOLVER
domain_action = 'restart'
- if dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
+ if dict_search_args(firewall, 'group', 'remote_group') or dict_search_args(firewall, 'group', 'domain_group') or firewall['ip_fqdn'].items() or firewall['ip6_fqdn'].items():
text = f'# Automatically generated by firewall.py\nThis file indicates that vyos-domain-resolver service is used by the firewall.\n'
Path(domain_resolver_usage).write_text(text)
else:
@@ -611,7 +637,7 @@ def apply(firewall):
# Call helper script to Update set contents
if 'name' in firewall['geoip_updated'] or 'ipv6_name' in firewall['geoip_updated']:
print('Updating GeoIP. Please wait...')
- geoip_update(firewall)
+ geoip_update(firewall=firewall)
return None
diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py
index 4f1141dcb..84316c16e 100755
--- a/src/conf_mode/interfaces_bonding.py
+++ b/src/conf_mode/interfaces_bonding.py
@@ -126,9 +126,8 @@ def get_config(config=None):
# Restore existing config level
conf.set_level(old_level)
- if dict_search('member.interface', bond):
- for interface, interface_config in bond['member']['interface'].items():
-
+ if dict_search('member.interface', bond) is not None:
+ for interface in bond['member']['interface']:
interface_ethernet_config = conf.get_config_dict(
['interfaces', 'ethernet', interface],
key_mangling=('-', '_'),
@@ -137,44 +136,45 @@ def get_config(config=None):
with_defaults=False,
with_recursive_defaults=False)
- interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config)
+ bond['member']['interface'][interface].update({'config_paths' :
+ dict_to_paths_values(interface_ethernet_config)})
# Check if member interface is a new member
if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]):
bond['shutdown_required'] = {}
- interface_config['new_added'] = {}
+ bond['member']['interface'][interface].update({'new_added' : {}})
# Check if member interface is disabled
conf.set_level(['interfaces'])
section = Section.section(interface) # this will be 'ethernet' for 'eth0'
if conf.exists([section, interface, 'disable']):
- interface_config['disable'] = ''
+ if tmp: bond['member']['interface'][interface].update({'disable': ''})
conf.set_level(old_level)
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp: interface_config['is_bridge_member'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
- for tmp in is_member(conf, interface, 'bonding'):
- if bond['ifname'] == tmp:
- continue
- interface_config['is_bond_member'] = tmp
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp})
# Check if member interface is used as source-interface on another interface
tmp = is_source_interface(conf, interface)
- if tmp: interface_config['is_source_interface'] = tmp
+ if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp})
# bond members must not have an assigned address
tmp = has_address_configured(conf, interface)
- if tmp: interface_config['has_address'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_address' : ''})
# bond members must not have a VRF attached
tmp = has_vrf_configured(conf, interface)
- if tmp: interface_config['has_vrf'] = {}
+ if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''})
+
return bond
diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py
index 637db442a..95dcc543e 100755
--- a/src/conf_mode/interfaces_bridge.py
+++ b/src/conf_mode/interfaces_bridge.py
@@ -25,6 +25,7 @@ from vyos.configdict import has_vlan_subinterface_configured
from vyos.configverify import verify_dhcpv6
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import BridgeIf
from vyos.configdict import has_address_configured
from vyos.configdict import has_vrf_configured
@@ -74,8 +75,9 @@ def get_config(config=None):
for interface in list(bridge['member']['interface']):
# Check if member interface is already member of another bridge
tmp = is_member(conf, interface, 'bridge')
- if tmp and bridge['ifname'] not in tmp:
- bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
+ if ifname in tmp:
+ del tmp[ifname]
+ if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp})
# Check if member interface is already member of a bond
tmp = is_member(conf, interface, 'bonding')
@@ -135,6 +137,7 @@ def verify(bridge):
verify_dhcpv6(bridge)
verify_vrf(bridge)
+ verify_mtu_ipv6(bridge)
verify_mirror_redirect(bridge)
ifname = bridge['ifname']
diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py
index 007708d4a..1c5b4d0e7 100755
--- a/src/conf_mode/interfaces_geneve.py
+++ b/src/conf_mode/interfaces_geneve.py
@@ -47,7 +47,7 @@ def get_config(config=None):
# GENEVE interfaces are picky and require recreation if certain parameters
# change. But a GENEVE interface should - of course - not be re-created if
# it's description or IP address is adjusted. Feels somehow logic doesn't it?
- for cli_option in ['remote', 'vni', 'parameters']:
+ for cli_option in ['remote', 'vni', 'parameters', 'port']:
if is_node_changed(conf, base + [ifname, cli_option]):
geneve.update({'rebuild_required': {}})
diff --git a/src/conf_mode/interfaces_pseudo-ethernet.py b/src/conf_mode/interfaces_pseudo-ethernet.py
index 446beffd3..b066fd542 100755
--- a/src/conf_mode/interfaces_pseudo-ethernet.py
+++ b/src/conf_mode/interfaces_pseudo-ethernet.py
@@ -27,6 +27,7 @@ from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_source_interface
from vyos.configverify import verify_vlan_config
from vyos.configverify import verify_mtu_parent
+from vyos.configverify import verify_mtu_ipv6
from vyos.configverify import verify_mirror_redirect
from vyos.ifconfig import MACVLANIf
from vyos.utils.network import interface_exists
@@ -71,6 +72,7 @@ def verify(peth):
verify_vrf(peth)
verify_address(peth)
verify_mtu_parent(peth, peth['parent'])
+ verify_mtu_ipv6(peth)
verify_mirror_redirect(peth)
# use common function to verify VLAN configuration
verify_vlan_config(peth)
diff --git a/src/conf_mode/interfaces_virtual-ethernet.py b/src/conf_mode/interfaces_virtual-ethernet.py
index cb6104f59..59ce474fc 100755
--- a/src/conf_mode/interfaces_virtual-ethernet.py
+++ b/src/conf_mode/interfaces_virtual-ethernet.py
@@ -23,6 +23,7 @@ from vyos.configdict import get_interface_dict
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import VethIf
from vyos.utils.network import interface_exists
airbag.enable()
@@ -62,6 +63,7 @@ def verify(veth):
return None
verify_vrf(veth)
+ verify_mtu_ipv6(veth)
verify_address(veth)
if 'peer_name' not in veth:
diff --git a/src/conf_mode/interfaces_vti.py b/src/conf_mode/interfaces_vti.py
index 20629c6c1..915bde066 100755
--- a/src/conf_mode/interfaces_vti.py
+++ b/src/conf_mode/interfaces_vti.py
@@ -20,6 +20,7 @@ from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import VTIIf
from vyos import ConfigError
from vyos import airbag
@@ -40,6 +41,7 @@ def get_config(config=None):
def verify(vti):
verify_vrf(vti)
+ verify_mtu_ipv6(vti)
verify_mirror_redirect(vti)
return None
diff --git a/src/conf_mode/interfaces_vxlan.py b/src/conf_mode/interfaces_vxlan.py
index 68646e8ff..256b65708 100755
--- a/src/conf_mode/interfaces_vxlan.py
+++ b/src/conf_mode/interfaces_vxlan.py
@@ -95,6 +95,8 @@ def verify(vxlan):
if 'group' in vxlan:
if 'source_interface' not in vxlan:
raise ConfigError('Multicast VXLAN requires an underlaying interface')
+ if 'remote' in vxlan:
+ raise ConfigError('Both group and remote cannot be specified')
verify_source_interface(vxlan)
if not any(tmp in ['group', 'remote', 'source_address', 'source_interface'] for tmp in vxlan):
diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py
index 877d013cf..3ca6ecdca 100755
--- a/src/conf_mode/interfaces_wireguard.py
+++ b/src/conf_mode/interfaces_wireguard.py
@@ -19,6 +19,9 @@ from sys import exit
from vyos.config import Config
from vyos.configdict import get_interface_dict
from vyos.configdict import is_node_changed
+from vyos.configdict import is_source_interface
+from vyos.configdep import set_dependents
+from vyos.configdep import call_dependents
from vyos.configverify import verify_vrf
from vyos.configverify import verify_address
from vyos.configverify import verify_bridge_delete
@@ -35,6 +38,7 @@ from vyos import airbag
from pathlib import Path
airbag.enable()
+
def get_config(config=None):
"""
Retrive CLI config as dictionary. Dictionary can never be empty, as at least the
@@ -61,11 +65,25 @@ def get_config(config=None):
if 'disable' not in peer_config and 'host_name' in peer_config:
wireguard['peers_need_resolve'].append(peer)
+ # Check if interface is used as source-interface on VXLAN interface
+ tmp = is_source_interface(conf, ifname, 'vxlan')
+ if tmp:
+ if 'deleted' not in wireguard:
+ set_dependents('vxlan', conf, tmp)
+ else:
+ wireguard['is_source_interface'] = tmp
+
return wireguard
+
def verify(wireguard):
if 'deleted' in wireguard:
verify_bridge_delete(wireguard)
+ if 'is_source_interface' in wireguard:
+ raise ConfigError(
+ f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used '
+ f'as source interface for "{wireguard["is_source_interface"]}"!'
+ )
return None
verify_mtu_ipv6(wireguard)
@@ -79,7 +97,7 @@ def verify(wireguard):
if 'port' in wireguard and 'port_changed' in wireguard:
listen_port = int(wireguard['port'])
- if check_port_availability('0.0.0.0', listen_port, 'udp') is not True:
+ if check_port_availability(None, listen_port, protocol='udp') is not True:
raise ConfigError(f'UDP port {listen_port} is busy or unavailable and '
'cannot be used for the interface!')
@@ -119,9 +137,11 @@ def verify(wireguard):
public_keys.append(peer['public_key'])
+
def generate(wireguard):
return None
+
def apply(wireguard):
check_kmod('wireguard')
@@ -157,8 +177,11 @@ def apply(wireguard):
domain_action = 'stop'
call(f'systemctl {domain_action} vyos-domain-resolver.service')
+ call_dependents()
+
return None
+
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/interfaces_wwan.py b/src/conf_mode/interfaces_wwan.py
index 230eb14d6..ddbebfb4a 100755
--- a/src/conf_mode/interfaces_wwan.py
+++ b/src/conf_mode/interfaces_wwan.py
@@ -26,6 +26,7 @@ from vyos.configverify import verify_authentication
from vyos.configverify import verify_interface_exists
from vyos.configverify import verify_mirror_redirect
from vyos.configverify import verify_vrf
+from vyos.configverify import verify_mtu_ipv6
from vyos.ifconfig import WWANIf
from vyos.utils.dict import dict_search
from vyos.utils.process import cmd
@@ -98,6 +99,7 @@ def verify(wwan):
verify_interface_exists(wwan, ifname)
verify_authentication(wwan)
verify_vrf(wwan)
+ verify_mtu_ipv6(wwan)
verify_mirror_redirect(wwan)
return None
diff --git a/src/conf_mode/load-balancing_haproxy.py b/src/conf_mode/load-balancing_haproxy.py
index 5fd1beec9..504a90596 100644
--- a/src/conf_mode/load-balancing_haproxy.py
+++ b/src/conf_mode/load-balancing_haproxy.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023-2024 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 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 shutil import rmtree
+from vyos.defaults import systemd_services
from vyos.config import Config
from vyos.configverify import verify_pki_certificate
from vyos.configverify import verify_pki_ca_certificate
@@ -39,7 +40,6 @@ airbag.enable()
load_balancing_dir = '/run/haproxy'
load_balancing_conf_file = f'{load_balancing_dir}/haproxy.cfg'
-systemd_service = 'haproxy.service'
systemd_override = '/run/systemd/system/haproxy.service.d/10-override.conf'
def get_config(config=None):
@@ -65,18 +65,18 @@ def verify(lb):
return None
if 'backend' not in lb or 'service' not in lb:
- raise ConfigError(f'"service" and "backend" must be configured!')
+ raise ConfigError('Both "service" and "backend" must be configured!')
for front, front_config in lb['service'].items():
if 'port' not in front_config:
raise ConfigError(f'"{front} service port" must be configured!')
# Check if bind address:port are used by another service
- tmp_address = front_config.get('address', '0.0.0.0')
+ tmp_address = front_config.get('address', None)
tmp_port = front_config['port']
if check_port_availability(tmp_address, int(tmp_port), 'tcp') is not True and \
not is_listen_port_bind_service(int(tmp_port), 'haproxy'):
- raise ConfigError(f'"TCP" port "{tmp_port}" is used by another service')
+ raise ConfigError(f'TCP port "{tmp_port}" is used by another service')
if 'http_compression' in front_config:
if front_config['mode'] != 'http':
@@ -85,16 +85,19 @@ def verify(lb):
raise ConfigError(f'service {front} must have at least one mime-type configured to use'
f'http_compression!')
+ for cert in dict_search('ssl.certificate', front_config) or []:
+ verify_pki_certificate(lb, cert)
+
for back, back_config in lb['backend'].items():
if 'http_check' in back_config:
http_check = back_config['http_check']
if 'expect' in http_check and 'status' in http_check['expect'] and 'string' in http_check['expect']:
- raise ConfigError(f'"expect status" and "expect string" can not be configured together!')
+ raise ConfigError('"expect status" and "expect string" can not be configured together!')
if 'health_check' in back_config:
if back_config['mode'] != 'tcp':
raise ConfigError(f'backend "{back}" can only be configured with {back_config["health_check"]} ' +
- f'health-check whilst in TCP mode!')
+ 'health-check whilst in TCP mode!')
if 'http_check' in back_config:
raise ConfigError(f'backend "{back}" cannot be configured with both http-check and health-check!')
@@ -112,20 +115,15 @@ def verify(lb):
if {'no_verify', 'ca_certificate'} <= set(back_config['ssl']):
raise ConfigError(f'backend {back} cannot have both ssl options no-verify and ca-certificate set!')
+ tmp = dict_search('ssl.ca_certificate', back_config)
+ if tmp: verify_pki_ca_certificate(lb, tmp)
+
# Check if http-response-headers are configured in any frontend/backend where mode != http
for group in ['service', 'backend']:
for config_name, config in lb[group].items():
if 'http_response_headers' in config and config['mode'] != 'http':
raise ConfigError(f'{group} {config_name} must be set to http mode to use http_response_headers!')
- for front, front_config in lb['service'].items():
- for cert in dict_search('ssl.certificate', front_config) or []:
- verify_pki_certificate(lb, cert)
-
- for back, back_config in lb['backend'].items():
- tmp = dict_search('ssl.ca_certificate', back_config)
- if tmp: verify_pki_ca_certificate(lb, tmp)
-
def generate(lb):
if not lb:
@@ -193,12 +191,11 @@ def generate(lb):
return None
def apply(lb):
+ action = 'stop'
+ if lb:
+ action = 'reload-or-restart'
call('systemctl daemon-reload')
- if not lb:
- call(f'systemctl stop {systemd_service}')
- else:
- call(f'systemctl reload-or-restart {systemd_service}')
-
+ call(f'systemctl {action} {systemd_services["haproxy"]}')
return None
diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py
index 5da0b906b..92d9acfba 100755
--- a/src/conf_mode/load-balancing_wan.py
+++ b/src/conf_mode/load-balancing_wan.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2023 VyOS maintainers and contributors
+# Copyright (C) 2023-2025 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,24 +14,16 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
-import os
-
from sys import exit
-from shutil import rmtree
-from vyos.base import Warning
from vyos.config import Config
from vyos.configdep import set_dependents, call_dependents
from vyos.utils.process import cmd
-from vyos.template import render
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-load_balancing_dir = '/run/load-balance'
-load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf'
-systemd_service = 'vyos-wan-load-balance.service'
-
+service = 'vyos-wan-load-balance.service'
def get_config(config=None):
if config:
@@ -40,6 +32,7 @@ def get_config(config=None):
conf = Config()
base = ['load-balancing', 'wan']
+
lb = conf.get_config_dict(base, key_mangling=('-', '_'),
no_tag_node_value_mangle=True,
get_first_key=True,
@@ -59,87 +52,61 @@ def verify(lb):
if not lb:
return None
- if 'interface_health' not in lb:
- raise ConfigError(
- 'A valid WAN load-balance configuration requires an interface with a nexthop!'
- )
-
- for interface, interface_config in lb['interface_health'].items():
- if 'nexthop' not in interface_config:
- raise ConfigError(
- f'interface-health {interface} nexthop must be specified!')
-
- if 'test' in interface_config:
- for test_rule, test_config in interface_config['test'].items():
- if 'type' in test_config:
- if test_config['type'] == 'user-defined' and 'test_script' not in test_config:
- raise ConfigError(
- f'test {test_rule} script must be defined for test-script!'
- )
-
- if 'rule' not in lb:
- Warning(
- 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!'
- )
+ if 'interface_health' in lb:
+ for ifname, health_conf in lb['interface_health'].items():
+ if 'nexthop' not in health_conf:
+ raise ConfigError(f'Nexthop must be configured for interface {ifname}')
+
+ if 'test' not in health_conf:
+ continue
+
+ for test_id, test_conf in health_conf['test'].items():
+ if 'type' not in test_conf:
+ raise ConfigError(f'No type configured for health test on interface {ifname}')
+
+ if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf:
+ raise ConfigError(f'Missing user-defined script for health test on interface {ifname}')
else:
- for rule, rule_config in lb['rule'].items():
- if 'inbound_interface' not in rule_config:
- raise ConfigError(f'rule {rule} inbound-interface must be specified!')
- if {'failover', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} failover cannot be configured with exclude!')
- if {'limit', 'exclude'} <= set(rule_config):
- raise ConfigError(f'rule {rule} limit cannot be used with exclude!')
- if 'interface' not in rule_config:
- if 'exclude' not in rule_config:
- Warning(
- f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule'
- )
- for direction in {'source', 'destination'}:
- if direction in rule_config:
- if 'protocol' in rule_config and 'port' in rule_config[
- direction]:
- if rule_config['protocol'] not in {'tcp', 'udp'}:
- raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"')
+ raise ConfigError('Interface health tests must be configured')
+ if 'rule' in lb:
+ for rule_id, rule_conf in lb['rule'].items():
+ if 'interface' not in rule_conf and 'exclude' not in rule_conf:
+ raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}')
-def generate(lb):
- if not lb:
- # Delete /run/load-balance/wlb.conf
- if os.path.isfile(load_balancing_conf_file):
- os.unlink(load_balancing_conf_file)
- # Delete old directories
- if os.path.isdir(load_balancing_dir):
- rmtree(load_balancing_dir, ignore_errors=True)
- if os.path.exists('/var/run/load-balance/wlb.out'):
- os.unlink('/var/run/load-balance/wlb.out')
+ if 'failover' in rule_conf and 'exclude' in rule_conf:
+ raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}')
- return None
+ if 'limit' in rule_conf:
+ if 'exclude' in rule_conf:
+ raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}')
- # Create load-balance dir
- if not os.path.isdir(load_balancing_dir):
- os.mkdir(load_balancing_dir)
+ if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}')
- render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb)
+ if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']:
+ raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}')
- return None
+ for direction in ['source', 'destination']:
+ if direction in rule_conf:
+ if 'port' in rule_conf[direction]:
+ if 'protocol' not in rule_conf:
+ raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}')
+
+ if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']:
+ raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}')
+def generate(lb):
+ return None
def apply(lb):
if not lb:
- try:
- cmd(f'systemctl stop {systemd_service}')
- except Exception as e:
- print(f"Error message: {e}")
-
+ cmd(f'sudo systemctl stop {service}')
else:
- cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1')
- cmd(f'systemctl restart {systemd_service}')
+ cmd(f'sudo systemctl restart {service}')
call_dependents()
- return None
-
-
if __name__ == '__main__':
try:
c = get_config()
diff --git a/src/conf_mode/nat66.py b/src/conf_mode/nat66.py
index 95dfae3a5..c65950c9e 100755
--- a/src/conf_mode/nat66.py
+++ b/src/conf_mode/nat66.py
@@ -92,6 +92,10 @@ def verify(nat):
if prefix != None:
if not is_ipv6(prefix):
raise ConfigError(f'{err_msg} source-prefix not specified')
+
+ if 'destination' in config and 'group' in config['destination']:
+ if len({'address_group', 'network_group', 'domain_group'} & set(config['destination']['group'])) > 1:
+ raise ConfigError('Only one address-group, network-group or domain-group can be specified')
if dict_search('destination.rule', nat):
for rule, config in dict_search('destination.rule', nat).items():
diff --git a/src/conf_mode/pki.py b/src/conf_mode/pki.py
index acea2c9be..869518dd9 100755
--- a/src/conf_mode/pki.py
+++ b/src/conf_mode/pki.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021-2024 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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 argv
from sys import exit
+from vyos.base import Message
from vyos.config import Config
from vyos.config import config_dict_merge
from vyos.configdep import set_dependents
@@ -27,6 +28,8 @@ from vyos.configdict import node_changed
from vyos.configdiff import Diff
from vyos.configdiff import get_config_diff
from vyos.defaults import directories
+from vyos.defaults import internal_ports
+from vyos.defaults import systemd_services
from vyos.pki import encode_certificate
from vyos.pki import is_ca_certificate
from vyos.pki import load_certificate
@@ -42,9 +45,11 @@ 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.file import read_file
+from vyos.utils.network import check_port_availability
from vyos.utils.process import call
from vyos.utils.process import cmd
from vyos.utils.process import is_systemd_service_active
+from vyos.utils.process import is_systemd_service_running
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -128,8 +133,20 @@ def certbot_request(name: str, config: dict, dry_run: bool=True):
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}'
+
+ listen_address = None
if 'listen_address' in config:
- tmp += f' --http-01-address {config["listen_address"]}'
+ listen_address = config['listen_address']
+
+ # When ACME is used behind a reverse proxy, we always bind to localhost
+ # whatever the CLI listen-address is configured for.
+ if ('haproxy' in dict_search('used_by', config) and
+ is_systemd_service_running(systemd_services['haproxy']) and
+ not check_port_availability(listen_address, 80)):
+ tmp += f' --http-01-address 127.0.0.1 --http-01-port {internal_ports["certbot_haproxy"]}'
+ elif listen_address:
+ tmp += f' --http-01-address {listen_address}'
+
# verify() does not need to actually request a cert but only test for plausability
if dry_run:
tmp += ' --dry-run'
@@ -150,14 +167,18 @@ def get_config(config=None):
if len(argv) > 1 and argv[1] == 'certbot_renew':
pki['certbot_renew'] = {}
- changed_keys = ['ca', 'certificate', 'dh', 'key-pair', 'openssh', 'openvpn']
+ # Walk through the list of sync_translate mapping and build a list
+ # which is later used to check if the node was changed in the CLI config
+ changed_keys = []
+ for value in sync_translate.values():
+ if value not in changed_keys:
+ changed_keys.append(value)
+ # Check for changes to said given keys in the CLI config
for key in changed_keys:
tmp = node_changed(conf, base + [key], recursive=True, expand_nodes=Diff.DELETE | Diff.ADD)
-
if 'changed' not in pki:
pki.update({'changed':{}})
-
pki['changed'].update({key.replace('-', '_') : tmp})
# We only merge on the defaults of there is a configuration at all
@@ -219,8 +240,8 @@ def get_config(config=None):
continue
path = search['path']
- path_str = ' '.join(path + found_path)
- #print(f'PKI: Updating config: {path_str} {item_name}')
+ path_str = ' '.join(path + found_path).replace('_','-')
+ Message(f'Updating configuration: "{path_str} {item_name}"')
if path[0] == 'interfaces':
ifname = found_path[0]
@@ -230,6 +251,24 @@ def get_config(config=None):
if not D.node_changed_presence(path):
set_dependents(path[1], conf)
+ # Check PKI certificates if they are auto-generated by ACME. If they are,
+ # traverse the current configuration and determine the service where the
+ # certificate is used by.
+ # Required to check if we might need to run certbot behing a reverse proxy.
+ if 'certificate' in pki:
+ for name, cert_config in pki['certificate'].items():
+ if 'acme' not in cert_config:
+ continue
+ if not dict_search('system.load_balancing.haproxy', pki):
+ continue
+ used_by = []
+ for cert_list, _ in dict_search_recursive(
+ pki['system']['load_balancing']['haproxy'], 'certificate'):
+ if name in cert_list:
+ used_by.append('haproxy')
+ if used_by:
+ pki['certificate'][name]['acme'].update({'used_by': used_by})
+
return pki
def is_valid_certificate(raw_data):
@@ -321,6 +360,15 @@ def verify(pki):
raise ConfigError(f'An email address is required to request '\
f'certificate for "{name}" via ACME!')
+ listen_address = None
+ if 'listen_address' in cert_conf['acme']:
+ listen_address = cert_conf['acme']['listen_address']
+
+ if 'used_by' not in cert_conf['acme']:
+ if not check_port_availability(listen_address, 80):
+ raise ConfigError('Port 80 is already in use and not available '\
+ f'to provide ACME challenge for "{name}"!')
+
if 'certbot_renew' not in pki:
# Only run the ACME command if something on this entity changed,
# as this is time intensive
@@ -374,27 +422,35 @@ def verify(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 the node is still present, we can skip the check
+ # as we are not deleting it
+ if node_present:
+ continue
- if not node_present:
- search_dict = dict_search_args(pki['system'], *search['path'])
-
- if not search_dict:
- continue
+ 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_str = " ".join(search['path'] + found_path)
- raise ConfigError(f'PKI object "{item_name}" still in use by "{path_str}"')
+ for found_name, found_path in dict_search_recursive(search_dict, key):
+ # Check if the name matches either by string compare, or beeing
+ # part of a list
+ if ((isinstance(found_name, str) and found_name == item_name) or
+ (isinstance(found_name, list) and item_name in found_name)):
+ # We do not support _ in CLI paths - this is only a convenience
+ # as we mangle all - to _, now it's time to reverse this!
+ path_str = ' '.join(search['path'] + found_path).replace('_','-')
+ object = changed_key.replace('_','-')
+ tmp = f'Embedded PKI {object} with name "{item_name}" is still '\
+ f'in use by CLI path "{path_str}"'
+ raise ConfigError(tmp)
return None
@@ -440,13 +496,21 @@ def generate(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
+ # There is no ACME/certbot managed certificate presend on the
+ # system, generate it
if name not in certbot_list_on_disk:
certbot_request(name, cert_conf['acme'], dry_run=False)
+ # Now that the certificate was properly generated we have
+ # the PEM files on disk. We need to add the certificate to
+ # certbot_list_on_disk to automatically import the CA chain
+ certbot_list_on_disk.append(name)
+ # We alredy had an ACME managed certificate on the system, but
+ # something changed in the configuration
elif changed_certificates != None and name in changed_certificates:
- # when something for the certificate changed, we should delete it
+ # Delete old ACME certificate first
if name in certbot_list_on_disk:
certbot_delete(name)
+ # Request new certificate via certbot
certbot_request(name, cert_conf['acme'], dry_run=False)
# Cleanup certbot configuration and certificates if no longer in use by CLI
@@ -482,7 +546,7 @@ def generate(pki):
if not ca_cert_present:
tmp = dict_search_args(pki, 'ca', f'{autochain_prefix}{cert}', 'certificate')
if not bool(tmp) or tmp != cert_chain_base64:
- print(f'Adding/replacing automatically imported CA certificate for "{cert}" ...')
+ Message(f'Add/replace automatically imported CA certificate for "{cert}"...')
add_cli_node(['pki', 'ca', f'{autochain_prefix}{cert}', 'certificate'], value=cert_chain_base64)
return None
diff --git a/src/conf_mode/policy_route.py b/src/conf_mode/policy_route.py
index 223175b8a..521764896 100755
--- a/src/conf_mode/policy_route.py
+++ b/src/conf_mode/policy_route.py
@@ -21,13 +21,16 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
+from vyos.configdiff import get_config_diff, Diff
from vyos.template import render
from vyos.utils.dict import dict_search_args
+from vyos.utils.dict import dict_search_recursive
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.utils.network import get_vrf_tableid
from vyos.defaults import rt_global_table
from vyos.defaults import rt_global_vrf
+from vyos.firewall import geoip_update
from vyos import ConfigError
from vyos import airbag
airbag.enable()
@@ -43,6 +46,43 @@ valid_groups = [
'interface_group'
]
+def geoip_updated(conf, policy):
+ diff = get_config_diff(conf)
+ node_diff = diff.get_child_nodes_diff(['policy'], expand_nodes=Diff.DELETE, recursive=True)
+
+ out = {
+ 'name': [],
+ 'ipv6_name': [],
+ 'deleted_name': [],
+ 'deleted_ipv6_name': []
+ }
+ updated = False
+
+ for key, path in dict_search_recursive(policy, 'geoip'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if (path[0] == 'route'):
+ out['name'].append(set_name)
+ elif (path[0] == 'route6'):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ out['ipv6_name'].append(set_name)
+
+ updated = True
+
+ if 'delete' in node_diff:
+ for key, path in dict_search_recursive(node_diff['delete'], 'geoip'):
+ set_name = f'GEOIP_CC_{path[0]}_{path[1]}_{path[3]}'
+ if (path[0] == 'route'):
+ out['deleted_name'].append(set_name)
+ elif (path[0] == 'route6'):
+ set_name = f'GEOIP_CC6_{path[0]}_{path[1]}_{path[3]}'
+ out['deleted_ipv6_name'].append(set_name)
+ updated = True
+
+ if updated:
+ return out
+
+ return False
+
def get_config(config=None):
if config:
conf = config
@@ -60,6 +100,7 @@ def get_config(config=None):
if 'dynamic_group' in policy['firewall_group']:
del policy['firewall_group']['dynamic_group']
+ policy['geoip_updated'] = geoip_updated(conf, policy)
return policy
def verify_rule(policy, name, rule_conf, ipv6, rule_id):
@@ -203,6 +244,12 @@ def apply(policy):
apply_table_marks(policy)
+ if policy['geoip_updated']:
+ # Call helper script to Update set contents
+ if 'name' in policy['geoip_updated'] or 'ipv6_name' in policy['geoip_updated']:
+ print('Updating GeoIP. Please wait...')
+ geoip_update(policy=policy)
+
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py
index c4af717af..99d8eb9d1 100755
--- a/src/conf_mode/protocols_bgp.py
+++ b/src/conf_mode/protocols_bgp.py
@@ -413,15 +413,19 @@ def verify(config_dict):
verify_route_map(afi_config['route_map'][tmp], bgp)
if 'route_reflector_client' in afi_config:
- peer_group_as = peer_config.get('remote_as')
+ peer_as = peer_config.get('remote_as')
- if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
+ if peer_as is not None and (peer_as != 'internal' and peer_as != bgp['system_as']):
raise ConfigError('route-reflector-client only supported for iBGP peers')
else:
+ # Check into the peer group for the remote as, if we are in a peer group, check in peer itself
if 'peer_group' in peer_config:
peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp)
- if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
- raise ConfigError('route-reflector-client only supported for iBGP peers')
+ elif neighbor == 'peer_group':
+ peer_group_as = peer_config.get('remote_as')
+
+ if peer_group_as is None or (peer_group_as != 'internal' and peer_group_as != bgp['system_as']):
+ raise ConfigError('route-reflector-client only supported for iBGP peers')
# T5833 not all AFIs are supported for VRF
if 'vrf' in bgp and 'address_family' in peer_config:
@@ -523,12 +527,21 @@ def verify(config_dict):
raise ConfigError(
'Please unconfigure import vrf commands before using vpn commands in dependent VRFs!')
+ if (dict_search('route_map.vrf.import', afi_config) is not None
+ or dict_search('import.vrf', afi_config) is not None):
# FRR error: please unconfigure vpn to vrf commands before
# using import vrf commands
- if 'vpn' in afi_config['import'] or dict_search('export.vpn', afi_config) != None:
+ if (dict_search('import.vpn', afi_config) is not None
+ or dict_search('export.vpn', afi_config) is not None):
raise ConfigError('Please unconfigure VPN to VRF commands before '\
'using "import vrf" commands!')
+ if (dict_search('route_map.vpn.import', afi_config) is not None
+ or dict_search('route_map.vpn.export', afi_config) is not None) :
+ raise ConfigError('Please unconfigure route-map VPN to VRF commands before '\
+ 'using "import vrf" commands!')
+
+
# Verify that the export/import route-maps do exist
for export_import in ['export', 'import']:
tmp = dict_search(f'route_map.vpn.{export_import}', afi_config)
diff --git a/src/conf_mode/service_console-server.py b/src/conf_mode/service_console-server.py
index b112add3f..b83c6dfb1 100755
--- a/src/conf_mode/service_console-server.py
+++ b/src/conf_mode/service_console-server.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2021 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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
@@ -98,6 +98,12 @@ def generate(proxy):
return None
def apply(proxy):
+ if not os.path.exists('/etc/dropbear/dropbear_rsa_host_key'):
+ call('dropbearkey -t rsa -s 4096 -f /etc/dropbear/dropbear_rsa_host_key')
+
+ if not os.path.exists('/etc/dropbear/dropbear_ecdsa_host_key'):
+ call('dropbearkey -t ecdsa -f /etc/dropbear/dropbear_ecdsa_host_key')
+
call('systemctl daemon-reload')
call('systemctl stop dropbear@*.service conserver-server.service')
diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py
index 5a729af74..99c7e6a1f 100755
--- a/src/conf_mode/service_dhcp-server.py
+++ b/src/conf_mode/service_dhcp-server.py
@@ -41,9 +41,9 @@ from vyos import airbag
airbag.enable()
-ctrl_config_file = '/run/kea/kea-ctrl-agent.conf'
ctrl_socket = '/run/kea/dhcp4-ctrl-socket'
config_file = '/run/kea/kea-dhcp4.conf'
+config_file_d2 = '/run/kea/kea-dhcp-ddns.conf'
lease_file = '/config/dhcp/dhcp4-leases.csv'
lease_file_glob = '/config/dhcp/dhcp4-leases*'
user_group = '_kea'
@@ -171,6 +171,15 @@ def get_config(config=None):
return dhcp
+def verify_ddns_domain_servers(domain_type, domain):
+ if 'dns_server' in domain:
+ invalid_servers = []
+ for server_no, server_config in domain['dns_server'].items():
+ if 'address' not in server_config:
+ invalid_servers.append(server_no)
+ if len(invalid_servers) > 0:
+ raise ConfigError(f'{domain_type} DNS servers {", ".join(invalid_servers)} in DDNS configuration need to have an IP address')
+ return None
def verify(dhcp):
# bail out early - looks like removal from running config
@@ -423,6 +432,22 @@ def verify(dhcp):
if not interface_exists(interface):
raise ConfigError(f'listen-interface "{interface}" does not exist')
+ if 'dynamic_dns_update' in dhcp:
+ ddns = dhcp['dynamic_dns_update']
+ if 'tsig_key' in ddns:
+ invalid_keys = []
+ for tsig_key_name, tsig_key_config in ddns['tsig_key'].items():
+ if not ('algorithm' in tsig_key_config and 'secret' in tsig_key_config):
+ invalid_keys.append(tsig_key_name)
+ if len(invalid_keys) > 0:
+ raise ConfigError(f'Both algorithm and secret need to be set for TSIG keys: {", ".join(invalid_keys)}')
+
+ if 'forward_domain' in ddns:
+ verify_ddns_domain_servers('Forward', ddns['forward_domain'])
+
+ if 'reverse_domain' in ddns:
+ verify_ddns_domain_servers('Reverse', ddns['reverse_domain'])
+
return None
@@ -480,25 +505,26 @@ def generate(dhcp):
dhcp['high_availability']['ca_cert_file'] = ca_cert_file
render(
- ctrl_config_file,
- 'dhcp-server/kea-ctrl-agent.conf.j2',
- dhcp,
- user=user_group,
- group=user_group,
- )
- render(
config_file,
'dhcp-server/kea-dhcp4.conf.j2',
dhcp,
user=user_group,
group=user_group,
)
+ if 'dynamic_dns_update' in dhcp:
+ render(
+ config_file_d2,
+ 'dhcp-server/kea-dhcp-ddns.conf.j2',
+ dhcp,
+ user=user_group,
+ group=user_group
+ )
return None
def apply(dhcp):
- services = ['kea-ctrl-agent', 'kea-dhcp4-server', 'kea-dhcp-ddns-server']
+ services = ['kea-dhcp4-server', 'kea-dhcp-ddns-server']
if not dhcp or 'disable' in dhcp:
for service in services:
@@ -515,9 +541,6 @@ def apply(dhcp):
if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp:
action = 'stop'
- if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp:
- action = 'stop'
-
call(f'systemctl {action} {service}.service')
return None
diff --git a/src/conf_mode/service_dns_forwarding.py b/src/conf_mode/service_dns_forwarding.py
index e3bdbc9f8..5636d6f83 100755
--- a/src/conf_mode/service_dns_forwarding.py
+++ b/src/conf_mode/service_dns_forwarding.py
@@ -366,6 +366,13 @@ def apply(dns):
hc.add_name_server_tags_recursor(['dhcp-' + interface,
'dhcpv6-' + interface ])
+ # add dhcp interfaces
+ if 'dhcp' in dns:
+ for interface in dns['dhcp']:
+ if interface_exists(interface):
+ hc.add_name_server_tags_recursor(['dhcp-' + interface,
+ 'dhcpv6-' + interface ])
+
# hostsd will generate the forward-zones file
# the list and keys() are required as get returns a dict, not list
hc.delete_forward_zones(list(hc.get_forward_zones().keys()))
diff --git a/src/conf_mode/service_https.py b/src/conf_mode/service_https.py
index 9e58b4c72..2123823f4 100755
--- a/src/conf_mode/service_https.py
+++ b/src/conf_mode/service_https.py
@@ -28,6 +28,7 @@ from vyos.configverify import verify_vrf
from vyos.configverify import verify_pki_certificate
from vyos.configverify import verify_pki_ca_certificate
from vyos.configverify import verify_pki_dh_parameters
+from vyos.configdiff import get_config_diff
from vyos.defaults import api_config_state
from vyos.pki import wrap_certificate
from vyos.pki import wrap_private_key
@@ -79,6 +80,14 @@ def get_config(config=None):
# merge CLI and default dictionary
https = config_dict_merge(default_values, https)
+
+ # some settings affecting nginx will require a restart:
+ # for example, a reload will not suffice when binding the listen address
+ # after nginx has started and dropped privileges; add flag here
+ diff = get_config_diff(conf)
+ children_changed = diff.node_changed_children(base)
+ https['nginx_restart_required'] = bool(set(children_changed) != set(['api']))
+
return https
def verify(https):
@@ -208,7 +217,10 @@ def apply(https):
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 https['nginx_restart_required']:
+ call(f'systemctl restart {https_service_name}')
+ else:
+ call(f'systemctl reload-or-restart {https_service_name}')
if __name__ == '__main__':
try:
diff --git a/src/conf_mode/service_ids_ddos-protection.py b/src/conf_mode/service_ids_ddos-protection.py
deleted file mode 100755
index 276a71fcb..000000000
--- a/src/conf_mode/service_ids_ddos-protection.py
+++ /dev/null
@@ -1,104 +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
-
-from sys import exit
-
-from vyos.config import Config
-from vyos.template import render
-from vyos.utils.process import call
-from vyos import ConfigError
-from vyos import airbag
-airbag.enable()
-
-config_file = r'/run/fastnetmon/fastnetmon.conf'
-networks_list = r'/run/fastnetmon/networks_list'
-excluded_networks_list = r'/run/fastnetmon/excluded_networks_list'
-attack_dir = '/var/log/fastnetmon_attacks'
-
-def get_config(config=None):
- if config:
- conf = config
- else:
- conf = Config()
- base = ['service', 'ids', 'ddos-protection']
- if not conf.exists(base):
- return None
-
- fastnetmon = conf.get_config_dict(base, key_mangling=('-', '_'),
- get_first_key=True,
- with_recursive_defaults=True)
-
- return fastnetmon
-
-def verify(fastnetmon):
- if not fastnetmon:
- return None
-
- if 'mode' not in fastnetmon:
- raise ConfigError('Specify operating mode!')
-
- if fastnetmon.get('mode') == 'mirror' and 'listen_interface' not in fastnetmon:
- raise ConfigError("Incorrect settings for 'mode mirror': must specify interface(s) for traffic mirroring")
-
- if fastnetmon.get('mode') == 'sflow' and 'listen_address' not in fastnetmon.get('sflow', {}):
- raise ConfigError("Incorrect settings for 'mode sflow': must specify sFlow 'listen-address'")
-
- if 'alert_script' in fastnetmon:
- if os.path.isfile(fastnetmon['alert_script']):
- # Check script permissions
- if not os.access(fastnetmon['alert_script'], os.X_OK):
- raise ConfigError('Script "{alert_script}" is not executable!'.format(fastnetmon['alert_script']))
- else:
- raise ConfigError('File "{alert_script}" does not exists!'.format(fastnetmon))
-
-def generate(fastnetmon):
- if not fastnetmon:
- for file in [config_file, networks_list]:
- if os.path.isfile(file):
- os.unlink(file)
-
- return None
-
- # Create dir for log attack details
- if not os.path.exists(attack_dir):
- os.mkdir(attack_dir)
-
- render(config_file, 'ids/fastnetmon.j2', fastnetmon)
- render(networks_list, 'ids/fastnetmon_networks_list.j2', fastnetmon)
- render(excluded_networks_list, 'ids/fastnetmon_excluded_networks_list.j2', fastnetmon)
- return None
-
-def apply(fastnetmon):
- systemd_service = 'fastnetmon.service'
- if not fastnetmon:
- # Stop fastnetmon service if removed
- call(f'systemctl stop {systemd_service}')
- else:
- call(f'systemctl reload-or-restart {systemd_service}')
-
- return None
-
-if __name__ == '__main__':
- try:
- c = get_config()
- verify(c)
- generate(c)
- apply(c)
- except ConfigError as e:
- print(e)
- exit(1)
diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py
index 1174b1238..c64c59af7 100755
--- a/src/conf_mode/service_snmp.py
+++ b/src/conf_mode/service_snmp.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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 vyos.base import Warning
from vyos.config import Config
from vyos.configdict import dict_merge
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
from vyos.snmpv3_hashgen import plaintext_to_md5
from vyos.snmpv3_hashgen import plaintext_to_sha1
from vyos.snmpv3_hashgen import random
@@ -43,7 +44,7 @@ config_file_access = r'/usr/share/snmp/snmpd.conf'
config_file_user = r'/var/lib/snmp/snmpd.conf'
default_script_dir = r'/config/user-data/'
systemd_override = r'/run/systemd/system/snmpd.service.d/override.conf'
-systemd_service = 'snmpd.service'
+systemd_service = systemd_services['snmpd']
def get_config(config=None):
if config:
@@ -146,6 +147,9 @@ def verify(snmp):
return None
if 'user' in snmp['v3']:
+ if 'engineid' not in snmp['v3']:
+ raise ConfigError(f'EngineID must be configured for SNMPv3!')
+
for user, user_config in snmp['v3']['user'].items():
if 'group' not in user_config:
raise ConfigError(f'Group membership required for user "{user}"!')
diff --git a/src/conf_mode/system_host-name.py b/src/conf_mode/system_host-name.py
index 3f245f166..de4accda2 100755
--- a/src/conf_mode/system_host-name.py
+++ b/src/conf_mode/system_host-name.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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 @@ import vyos.hostsd_client
from vyos.base import Warning
from vyos.config import Config
from vyos.configdict import leaf_node_changed
+from vyos.defaults import systemd_services
from vyos.ifconfig import Section
from vyos.template import is_ip
from vyos.utils.process import cmd
@@ -174,11 +175,13 @@ def apply(config):
# Restart services that use the hostname
if hostname_new != hostname_old:
- call("systemctl restart rsyslog.service")
+ tmp = systemd_services['syslog']
+ call(f'systemctl restart {tmp}')
# If SNMP is running, restart it too
if process_named_running('snmpd') and config['snmpd_restart_reqired']:
- call('systemctl restart snmpd.service')
+ tmp = systemd_services['snmpd']
+ call(f'systemctl restart {tmp}')
return None
diff --git a/src/conf_mode/system_login.py b/src/conf_mode/system_login.py
index d3a969d9b..4febb6494 100755
--- a/src/conf_mode/system_login.py
+++ b/src/conf_mode/system_login.py
@@ -24,10 +24,13 @@ from pwd import getpwuid
from sys import exit
from time import sleep
+from vyos.base import Warning
from vyos.config import Config
from vyos.configverify import verify_vrf
from vyos.template import render
from vyos.template import is_ipv4
+from vyos.utils.auth import EPasswdStrength
+from vyos.utils.auth import evaluate_strength
from vyos.utils.auth import get_current_user
from vyos.utils.configfs import delete_cli_node
from vyos.utils.configfs import add_cli_node
@@ -146,6 +149,19 @@ def verify(login):
if s_user.pw_name == user and s_user.pw_uid < MIN_USER_UID:
raise ConfigError(f'User "{user}" can not be created, conflict with local system account!')
+ # T6353: Check password for complexity using cracklib.
+ # A user password should be sufficiently complex
+ plaintext_password = dict_search(
+ path='authentication.plaintext_password',
+ dict_object=user_config
+ ) or None
+
+ failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
+ if plaintext_password is not None:
+ result = evaluate_strength(plaintext_password)
+ if result['strength'] in failed_check_status:
+ Warning(result['error'])
+
for pubkey, pubkey_options in (dict_search('authentication.public_keys', user_config) or {}).items():
if 'type' not in pubkey_options:
raise ConfigError(f'Missing type for public-key "{pubkey}"!')
diff --git a/src/conf_mode/system_login_banner.py b/src/conf_mode/system_login_banner.py
index 5826d8042..cdd066649 100755
--- a/src/conf_mode/system_login_banner.py
+++ b/src/conf_mode/system_login_banner.py
@@ -95,8 +95,12 @@ def apply(banner):
render(POSTLOGIN_FILE, 'login/default_motd.j2', banner,
permission=0o644, user='root', group='root')
- render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2', banner,
- permission=0o644, user='root', group='root')
+ if banner['version_data']['build_type'] != 'release':
+ render(POSTLOGIN_VYOS_FILE, 'login/motd_vyos_nonproduction.j2',
+ banner,
+ permission=0o644,
+ user='root',
+ group='root')
return None
diff --git a/src/conf_mode/system_option.py b/src/conf_mode/system_option.py
index e2832cde6..5acad6599 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-2024 VyOS maintainers and contributors
+# Copyright (C) 2019-2025 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
@@ -86,7 +86,7 @@ def verify(options):
if 'source_address' in config:
if not is_addr_assigned(config['source_address']):
- raise ConfigError('No interface with give address specified!')
+ raise ConfigError('No interface with given address specified!')
if 'ssh_client' in options:
config = options['ssh_client']
@@ -122,7 +122,14 @@ def generate(options):
render(ssh_config, 'system/ssh_config.j2', options)
render(usb_autosuspend, 'system/40_usb_autosuspend.j2', options)
+ # XXX: This code path and if statements must be kept in sync with the Kernel
+ # option handling in image_installer.py:get_cli_kernel_options(). This
+ # occurance is used for having the appropriate options passed to GRUB
+ # when re-configuring options on the CLI.
cmdline_options = []
+ kernel_opts = options.get('kernel', {})
+ k_cpu_opts = kernel_opts.get('cpu', {})
+ k_memory_opts = kernel_opts.get('memory', {})
if 'kernel' in options:
if 'disable_mitigations' in options['kernel']:
cmdline_options.append('mitigations=off')
@@ -131,8 +138,51 @@ def generate(options):
if 'amd_pstate_driver' in options['kernel']:
mode = options['kernel']['amd_pstate_driver']
cmdline_options.append(
- f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}'
- )
+ f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}')
+ if 'quiet' in options['kernel']:
+ cmdline_options.append('quiet')
+
+ if 'disable_hpet' in kernel_opts:
+ cmdline_options.append('hpet=disable')
+
+ if 'disable_mce' in kernel_opts:
+ cmdline_options.append('mce=off')
+
+ if 'disable_softlockup' in kernel_opts:
+ cmdline_options.append('nosoftlockup')
+
+ # CPU options
+ isol_cpus = k_cpu_opts.get('isolate_cpus')
+ if isol_cpus:
+ cmdline_options.append(f'isolcpus={isol_cpus}')
+
+ nohz_full = k_cpu_opts.get('nohz_full')
+ if nohz_full:
+ cmdline_options.append(f'nohz_full={nohz_full}')
+
+ rcu_nocbs = k_cpu_opts.get('rcu_no_cbs')
+ if rcu_nocbs:
+ cmdline_options.append(f'rcu_nocbs={rcu_nocbs}')
+
+ if 'disable_nmi_watchdog' in k_cpu_opts:
+ cmdline_options.append('nmi_watchdog=0')
+
+ # Memory options
+ if 'disable_numa_balancing' in k_memory_opts:
+ cmdline_options.append('numa_balancing=disable')
+
+ default_hp_size = k_memory_opts.get('default_hugepage_size')
+ if default_hp_size:
+ cmdline_options.append(f'default_hugepagesz={default_hp_size}')
+
+ hp_sizes = k_memory_opts.get('hugepage_size')
+ if hp_sizes:
+ for size, settings in hp_sizes.items():
+ cmdline_options.append(f'hugepagesz={size}')
+ count = settings.get('hugepage_count')
+ if count:
+ cmdline_options.append(f'hugepages={count}')
+
grub_util.update_kernel_cmdline_options(' '.join(cmdline_options))
return None
diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py
index 41119b494..a22dac36f 100755
--- a/src/conf_mode/system_sflow.py
+++ b/src/conf_mode/system_sflow.py
@@ -54,7 +54,7 @@ def verify(sflow):
# Check if configured sflow agent-address exist in the system
if 'agent_address' in sflow:
tmp = sflow['agent_address']
- if not is_addr_assigned(tmp):
+ if not is_addr_assigned(tmp, include_vrf=True):
raise ConfigError(
f'Configured "sflow agent-address {tmp}" does not exist in the system!'
)
diff --git a/src/conf_mode/system_syslog.py b/src/conf_mode/system_syslog.py
index eb2f02eb3..bdab09f3c 100755
--- a/src/conf_mode/system_syslog.py
+++ b/src/conf_mode/system_syslog.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2018-2024 VyOS maintainers and contributors
+# Copyright (C) 2018-2025 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,17 +20,22 @@ from sys import exit
from vyos.base import Warning
from vyos.config import Config
-from vyos.configdict import is_node_changed
from vyos.configverify import verify_vrf
+from vyos.defaults import systemd_services
+from vyos.utils.network import is_addr_assigned
from vyos.utils.process import call
from vyos.template import render
+from vyos.template import is_ipv4
+from vyos.template import is_ipv6
from vyos import ConfigError
from vyos import airbag
airbag.enable()
-rsyslog_conf = '/etc/rsyslog.d/00-vyos.conf'
+rsyslog_conf = '/run/rsyslog/rsyslog.conf'
logrotate_conf = '/etc/logrotate.d/vyos-rsyslog'
-systemd_override = r'/run/systemd/system/rsyslog.service.d/override.conf'
+
+systemd_socket = 'syslog.socket'
+systemd_service = systemd_services['syslog']
def get_config(config=None):
if config:
@@ -46,23 +51,17 @@ def get_config(config=None):
syslog.update({ 'logrotate' : logrotate_conf })
- tmp = is_node_changed(conf, base + ['vrf'])
- if tmp: syslog.update({'restart_required': {}})
-
syslog = conf.merge_defaults(syslog, recursive=True)
- if syslog.from_defaults(['global']):
- del syslog['global']
-
- if (
- 'global' in syslog
- and 'preserve_fqdn' in syslog['global']
- and conf.exists(['system', 'host-name'])
- and conf.exists(['system', 'domain-name'])
- ):
- hostname = conf.return_value(['system', 'host-name'])
- domain = conf.return_value(['system', 'domain-name'])
- fqdn = f'{hostname}.{domain}'
- syslog['global']['local_host_name'] = fqdn
+ if syslog.from_defaults(['local']):
+ del syslog['local']
+
+ if 'preserve_fqdn' in syslog:
+ if conf.exists(['system', 'host-name']):
+ tmp = conf.return_value(['system', 'host-name'])
+ syslog['preserve_fqdn']['host_name'] = tmp
+ if conf.exists(['system', 'domain-name']):
+ tmp = conf.return_value(['system', 'domain-name'])
+ syslog['preserve_fqdn']['domain_name'] = tmp
return syslog
@@ -70,13 +69,33 @@ def verify(syslog):
if not syslog:
return None
- if 'host' in syslog:
- for host, host_options in syslog['host'].items():
- if 'protocol' in host_options and host_options['protocol'] == 'udp':
- if 'format' in host_options and 'octet_counted' in host_options['format']:
- Warning(f'Syslog UDP transport for "{host}" should not use octet-counted format!')
-
- verify_vrf(syslog)
+ if 'preserve_fqdn' in syslog:
+ if 'host_name' not in syslog['preserve_fqdn']:
+ Warning('No "system host-name" defined - cannot set syslog FQDN!')
+ if 'domain_name' not in syslog['preserve_fqdn']:
+ Warning('No "system domain-name" defined - cannot set syslog FQDN!')
+
+ if 'remote' in syslog:
+ for remote, remote_options in syslog['remote'].items():
+ if 'protocol' in remote_options and remote_options['protocol'] == 'udp':
+ if 'format' in remote_options and 'octet_counted' in remote_options['format']:
+ Warning(f'Syslog UDP transport for "{remote}" should not use octet-counted format!')
+
+ if 'vrf' in remote_options:
+ verify_vrf(remote_options)
+
+ if 'source_address' in remote_options:
+ vrf = None
+ if 'vrf' in remote_options:
+ vrf = remote_options['vrf']
+ if not is_addr_assigned(remote_options['source_address'], vrf):
+ raise ConfigError('No interface with given address specified!')
+
+ source_address = remote_options['source_address']
+ if ((is_ipv4(remote) and is_ipv6(source_address)) or
+ (is_ipv6(remote) and is_ipv4(source_address))):
+ raise ConfigError(f'Source-address "{source_address}" does not match '\
+ f'address-family of remote "{remote}"!')
def generate(syslog):
if not syslog:
@@ -88,26 +107,15 @@ def generate(syslog):
return None
render(rsyslog_conf, 'rsyslog/rsyslog.conf.j2', syslog)
- render(systemd_override, 'rsyslog/override.conf.j2', syslog)
render(logrotate_conf, 'rsyslog/logrotate.j2', syslog)
-
- # Reload systemd manager configuration
- call('systemctl daemon-reload')
return None
def apply(syslog):
- systemd_socket = 'syslog.socket'
- systemd_service = 'syslog.service'
if not syslog:
call(f'systemctl stop {systemd_service} {systemd_socket}')
return None
- # we need to restart the service if e.g. the VRF name changed
- systemd_action = 'reload-or-restart'
- if 'restart_required' in syslog:
- systemd_action = 'restart'
-
- call(f'systemctl {systemd_action} {systemd_service}')
+ call(f'systemctl reload-or-restart {systemd_service}')
return None
if __name__ == '__main__':
diff --git a/src/conf_mode/vpn_ipsec.py b/src/conf_mode/vpn_ipsec.py
index 25604d2a2..2754314f7 100755
--- a/src/conf_mode/vpn_ipsec.py
+++ b/src/conf_mode/vpn_ipsec.py
@@ -64,6 +64,7 @@ swanctl_dir = '/etc/swanctl'
charon_conf = '/etc/strongswan.d/charon.conf'
charon_dhcp_conf = '/etc/strongswan.d/charon/dhcp.conf'
charon_radius_conf = '/etc/strongswan.d/charon/eap-radius.conf'
+charon_systemd_conf = '/etc/strongswan.d/charon-systemd.conf'
interface_conf = '/etc/strongswan.d/interfaces_use.conf'
swanctl_conf = f'{swanctl_dir}/swanctl.conf'
@@ -156,6 +157,8 @@ def get_config(config=None):
_, vti = get_interface_dict(conf, ['interfaces', 'vti'], vti_interface)
ipsec['vti_interface_dicts'][vti_interface] = vti
+ ipsec['vpp_ipsec_exists'] = conf.exists(['vpp', 'settings', 'ipsec'])
+
return ipsec
def get_dhcp_address(iface):
@@ -484,6 +487,17 @@ def verify(ipsec):
else:
raise ConfigError(f"Missing ike-group on site-to-site peer {peer}")
+ # verify encryption algorithm compatibility for IKE with VPP
+ if ipsec['vpp_ipsec_exists']:
+ ike_group = ipsec['ike_group'][peer_conf['ike_group']]
+ for proposal, proposal_config in ike_group.get('proposal', {}).items():
+ algs = ['gmac', 'serpent', 'twofish']
+ if any(alg in proposal_config['encryption'] for alg in algs):
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for IKE proposal {proposal} for site-to-site peer {peer} with VPP'
+ )
+
if 'authentication' not in peer_conf or 'mode' not in peer_conf['authentication']:
raise ConfigError(f"Missing authentication on site-to-site peer {peer}")
@@ -562,7 +576,7 @@ def verify(ipsec):
esp_group_name = tunnel_conf['esp_group'] if 'esp_group' in tunnel_conf else peer_conf['default_esp_group']
- if esp_group_name not in ipsec['esp_group']:
+ if esp_group_name not in ipsec.get('esp_group'):
raise ConfigError(f"Invalid esp-group on tunnel {tunnel} for site-to-site peer {peer}")
esp_group = ipsec['esp_group'][esp_group_name]
@@ -574,6 +588,18 @@ def verify(ipsec):
if ('local' in tunnel_conf and 'prefix' in tunnel_conf['local']) or ('remote' in tunnel_conf and 'prefix' in tunnel_conf['remote']):
raise ConfigError(f"Local/remote prefix cannot be used with ESP transport mode on tunnel {tunnel} for site-to-site peer {peer}")
+ # verify ESP encryption algorithm compatibility with VPP
+ # because Marvel plugin for VPP doesn't support all algorithms that Strongswan does
+ if ipsec['vpp_ipsec_exists']:
+ for proposal, proposal_config in esp_group.get('proposal', {}).items():
+ algs = ['aes128', 'aes192', 'aes256', 'aes128gcm128', 'aes192gcm128', 'aes256gcm128']
+ if proposal_config['encryption'] not in algs:
+ raise ConfigError(
+ f'Encryption algorithm {proposal_config["encryption"]} cannot be used '
+ f'for ESP proposal {proposal} on tunnel {tunnel} for site-to-site peer {peer} with VPP'
+ )
+
+
def cleanup_pki_files():
for path in [CERT_PATH, CA_PATH, CRL_PATH, KEY_PATH, PUBKEY_PATH]:
if not os.path.exists(path):
@@ -720,6 +746,7 @@ def generate(ipsec):
render(charon_conf, 'ipsec/charon.j2', ipsec)
render(charon_dhcp_conf, 'ipsec/charon/dhcp.conf.j2', ipsec)
render(charon_radius_conf, 'ipsec/charon/eap-radius.conf.j2', ipsec)
+ render(charon_systemd_conf, 'ipsec/charon_systemd.conf.j2', ipsec)
render(interface_conf, 'ipsec/interfaces_use.conf.j2', ipsec)
render(swanctl_conf, 'ipsec/swanctl.conf.j2', ipsec)
diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute b/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute
new file mode 100644
index 000000000..38f674276
--- /dev/null
+++ b/src/etc/dhcp/dhclient-enter-hooks.d/06-vyos-nodefaultroute
@@ -0,0 +1,20 @@
+# Don't add default route if no-default-route is configured for interface
+
+# As configuration is not available to cli-shell-api at the first boot, we must use vyos.config, which contains a workaround for this
+function get_no_default_route {
+python3 - <<PYEND
+from vyos.config import Config
+import os
+
+config = Config()
+if config.exists('interfaces'):
+ iface_types = config.list_nodes('interfaces')
+ for iface_type in iface_types:
+ if config.exists("interfaces {} {} dhcp-options no-default-route".format(iface_type, os.environ['interface'])):
+ print("True")
+PYEND
+}
+
+if [[ "$(get_no_default_route)" == 'True' ]]; then
+ new_routers=""
+fi
diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client
index 55d15a163..a230fe900 100755
--- a/src/etc/netplug/vyos-netplug-dhcp-client
+++ b/src/etc/netplug/vyos-netplug-dhcp-client
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2025 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
@@ -19,44 +19,53 @@ import sys
from time import sleep
-from vyos.configquery import ConfigTreeQuery
+from vyos.config import Config
from vyos.ifconfig import Section
from vyos.utils.boot import boot_configuration_complete
+from vyos.utils.process import cmd
+from vyos.utils.process import is_systemd_service_active
from vyos.utils.commit import commit_in_progress
-from vyos.utils.process import call
from vyos import airbag
+
airbag.enable()
if len(sys.argv) < 3:
- airbag.noteworthy("Must specify both interface and link status!")
+ airbag.noteworthy('Must specify both interface and link status!')
sys.exit(1)
if not boot_configuration_complete():
- airbag.noteworthy("System bootup not yet finished...")
+ airbag.noteworthy('System bootup not yet finished...')
sys.exit(1)
+interface = sys.argv[1]
+
while commit_in_progress():
- sleep(1)
+ sleep(0.250)
-interface = sys.argv[1]
in_out = sys.argv[2]
-config = ConfigTreeQuery()
+config = Config()
interface_path = ['interfaces'] + Section.get_config_path(interface).split()
-for _, interface_config in config.get_config_dict(interface_path).items():
- # Bail out early if we do not have an IP address configured
- if 'address' not in interface_config:
- continue
- # Bail out early if interface ist administrative down
- if 'disable' in interface_config:
- continue
- systemd_action = 'start'
- if in_out == 'out':
- systemd_action = 'stop'
- # Start/Stop DHCP service
- if 'dhcp' in interface_config['address']:
- call(f'systemctl {systemd_action} dhclient@{interface}.service')
- # Start/Stop DHCPv6 service
- if 'dhcpv6' in interface_config['address']:
- call(f'systemctl {systemd_action} dhcp6c@{interface}.service')
+systemdV4_service = f'dhclient@{interface}.service'
+systemdV6_service = f'dhcp6c@{interface}.service'
+if in_out == 'out':
+ # Interface moved state to down
+ if is_systemd_service_active(systemdV4_service):
+ cmd(f'systemctl stop {systemdV4_service}')
+ if is_systemd_service_active(systemdV6_service):
+ cmd(f'systemctl stop {systemdV6_service}')
+elif in_out == 'in':
+ if config.exists_effective(interface_path + ['address']):
+ tmp = config.return_effective_values(interface_path + ['address'])
+ # Always (re-)start the DHCP(v6) client service. If the DHCP(v6) client
+ # is already running - which could happen if the interface is re-
+ # configured in operational down state, it will have a backoff
+ # time increasing while not receiving a DHCP(v6) reply.
+ #
+ # To make the interface instantly available, and as for a DHCP(v6) lease
+ # we will re-start the service and thus cancel the backoff time.
+ if 'dhcp' in tmp:
+ cmd(f'systemctl restart {systemdV4_service}')
+ if 'dhcpv6' in tmp:
+ cmd(f'systemctl restart {systemdV6_service}')
diff --git a/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb
new file mode 100755
index 000000000..fff258afa
--- /dev/null
+++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb
@@ -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/>.
+
+# This is a Python hook script which is invoked whenever a PPPoE session goes
+# "ip-up". It will call into our vyos.ifconfig library and will then execute
+# common tasks for the PPPoE interface. The reason we have to "hook" this is
+# that we can not create a pppoeX interface in advance in linux and then connect
+# pppd to this already existing interface.
+
+import os
+import signal
+
+from sys import argv
+from sys import exit
+
+from vyos.defaults import directories
+
+# When the ppp link comes up, this script is called with the following
+# parameters
+# $1 the interface name used by pppd (e.g. ppp3)
+# $2 the tty device name
+# $3 the tty device speed
+# $4 the local IP address for the interface
+# $5 the remote IP address
+# $6 the parameter specified by the 'ipparam' option to pppd
+
+if (len(argv) < 7):
+ exit(1)
+
+wlb_pid_file = '/run/wlb_daemon.pid'
+
+interface = argv[6]
+nexthop = argv[5]
+
+if not os.path.exists(directories['ppp_nexthop_dir']):
+ os.mkdir(directories['ppp_nexthop_dir'])
+
+nexthop_file = os.path.join(directories['ppp_nexthop_dir'], interface)
+
+with open(nexthop_file, 'w') as f:
+ f.write(nexthop)
+
+# Trigger WLB daemon update
+if os.path.exists(wlb_pid_file):
+ with open(wlb_pid_file, 'r') as f:
+ pid = int(f.read())
+
+ os.kill(pid, signal.SIGUSR2)
diff --git a/src/etc/rsyslog.conf b/src/etc/rsyslog.conf
deleted file mode 100644
index b3f41acb6..000000000
--- a/src/etc/rsyslog.conf
+++ /dev/null
@@ -1,67 +0,0 @@
-#################
-#### MODULES ####
-#################
-
-$ModLoad imuxsock # provides support for local system logging
-$ModLoad imklog # provides kernel logging support (previously done by rklogd)
-#$ModLoad immark # provides --MARK-- message capability
-
-$OmitLocalLogging off
-$SystemLogSocketName /run/systemd/journal/syslog
-
-$KLogPath /proc/kmsg
-
-###########################
-#### GLOBAL DIRECTIVES ####
-###########################
-
-# Use traditional timestamp format.
-# To enable high precision timestamps, comment out the following line.
-# A modern-style logfile format similar to TraditionalFileFormat, buth with high-precision timestamps and timezone information
-#$ActionFileDefaultTemplate RSYSLOG_FileFormat
-# The "old style" default log file format with low-precision timestamps
-$ActionFileDefaultTemplate RSYSLOG_TraditionalFileFormat
-
-# Filter duplicated messages
-$RepeatedMsgReduction on
-
-#
-# Set the default permissions for all log files.
-#
-$FileOwner root
-$FileGroup adm
-$FileCreateMode 0640
-$DirCreateMode 0755
-$Umask 0022
-
-#
-# Stop excessive logging of sudo
-#
-:msg, contains, " pam_unix(sudo:session): session opened for user root(uid=0) by" stop
-:msg, contains, "pam_unix(sudo:session): session closed for user root" stop
-
-#
-# Include all config files in /etc/rsyslog.d/
-#
-$IncludeConfig /etc/rsyslog.d/*.conf
-
-# The lines below cause all listed daemons/processes to be logged into
-# /var/log/auth.log, then drops the message so it does not also go to the
-# regular syslog so that messages are not duplicated
-
-$outchannel auth_log,/var/log/auth.log
-if $programname == 'CRON' or
- $programname == 'sudo' or
- $programname == 'su'
- then :omfile:$auth_log
-
-if $programname == 'CRON' or
- $programname == 'sudo' or
- $programname == 'su'
- then stop
-
-###############
-#### RULES ####
-###############
-# Emergencies are sent to everybody logged in.
-*.emerg :omusrmsg:* \ No newline at end of file
diff --git a/src/etc/sysctl.d/30-vyos-router.conf b/src/etc/sysctl.d/30-vyos-router.conf
index 76be41ddc..ef81cebac 100644
--- a/src/etc/sysctl.d/30-vyos-router.conf
+++ b/src/etc/sysctl.d/30-vyos-router.conf
@@ -83,6 +83,16 @@ net.ipv4.conf.default.ignore_routes_with_linkdown=1
net.ipv6.conf.all.ignore_routes_with_linkdown=1
net.ipv6.conf.default.ignore_routes_with_linkdown=1
+# Disable IPv6 interface autoconfigurationnable packet forwarding for IPv6
+net.ipv6.conf.all.autoconf=0
+net.ipv6.conf.default.autoconf=0
+net.ipv6.conf.*.autoconf=0
+
+# Disable IPv6 router advertisements
+net.ipv6.conf.all.accept_ra=0
+net.ipv6.conf.default.accept_ra=0
+net.ipv6.conf.*.accept_ra=0
+
# Enable packet forwarding for IPv6
net.ipv6.conf.all.forwarding=1
diff --git a/src/etc/systemd/system/fastnetmon.service.d/override.conf b/src/etc/systemd/system/fastnetmon.service.d/override.conf
deleted file mode 100644
index 841666070..000000000
--- a/src/etc/systemd/system/fastnetmon.service.d/override.conf
+++ /dev/null
@@ -1,12 +0,0 @@
-[Unit]
-RequiresMountsFor=/run
-ConditionPathExists=/run/fastnetmon/fastnetmon.conf
-After=
-After=vyos-router.service
-
-[Service]
-Type=simple
-WorkingDirectory=/run/fastnetmon
-PIDFile=/run/fastnetmon.pid
-ExecStart=
-ExecStart=/usr/sbin/fastnetmon --configuration_file /run/fastnetmon/fastnetmon.conf
diff --git a/src/etc/systemd/system/frr.service.d/override.conf b/src/etc/systemd/system/frr.service.d/override.conf
index 614b4f7ed..a4a73ecd9 100644
--- a/src/etc/systemd/system/frr.service.d/override.conf
+++ b/src/etc/systemd/system/frr.service.d/override.conf
@@ -3,9 +3,11 @@ After=vyos-router.service
[Service]
LimitNOFILE=4096
-ExecStartPre=/bin/bash -c 'mkdir -p /run/frr/config; \
+ExecStartPre=/bin/bash -c 'if [ ! -f /run/frr/config/frr.conf ]; then \
+ mkdir -p /run/frr/config; \
echo "log syslog" > /run/frr/config/frr.conf; \
echo "log facility local7" >> /run/frr/config/frr.conf; \
chown frr:frr /run/frr/config/frr.conf; \
chmod 664 /run/frr/config/frr.conf; \
- mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf'
+ mount --bind /run/frr/config/frr.conf /etc/frr/frr.conf; \
+fi;'
diff --git a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf b/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
deleted file mode 100644
index c74fafb42..000000000
--- a/src/etc/systemd/system/kea-ctrl-agent.service.d/override.conf
+++ /dev/null
@@ -1,10 +0,0 @@
-[Unit]
-After=
-After=vyos-router.service
-ConditionFileNotEmpty=
-
-[Service]
-ExecStart=
-ExecStart=/usr/sbin/kea-ctrl-agent -c /run/kea/kea-ctrl-agent.conf
-AmbientCapabilities=CAP_NET_BIND_SERVICE
-CapabilityBoundingSet=CAP_NET_BIND_SERVICE
diff --git a/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf
new file mode 100644
index 000000000..cdfdea8eb
--- /dev/null
+++ b/src/etc/systemd/system/kea-dhcp-ddns-server.service.d/override.conf
@@ -0,0 +1,7 @@
+[Unit]
+After=
+After=vyos-router.service
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/kea-dhcp-ddns -c /run/kea/kea-dhcp-ddns.conf
diff --git a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf
index 682e5bbce..4a04892c0 100644
--- a/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf
+++ b/src/etc/systemd/system/kea-dhcp4-server.service.d/override.conf
@@ -5,3 +5,5 @@ After=vyos-router.service
[Service]
ExecStart=
ExecStart=/usr/sbin/kea-dhcp4 -c /run/kea/kea-dhcp4.conf
+ExecStartPost=!/usr/bin/python3 /usr/libexec/vyos/system/sync-dhcp-lease-to-hosts.py --inet
+Restart=on-failure
diff --git a/src/etc/systemd/system/rsyslog.service.d/override.conf b/src/etc/systemd/system/rsyslog.service.d/override.conf
new file mode 100644
index 000000000..665b994d9
--- /dev/null
+++ b/src/etc/systemd/system/rsyslog.service.d/override.conf
@@ -0,0 +1,10 @@
+[Unit]
+StartLimitIntervalSec=0
+
+[Service]
+ExecStart=
+ExecStart=/usr/sbin/rsyslogd -n -iNONE -f /run/rsyslog/rsyslog.conf
+Restart=always
+RestartPreventExitStatus=
+RestartSec=10
+RuntimeDirectoryPreserve=yes
diff --git a/src/helpers/geoip-update.py b/src/helpers/geoip-update.py
index 34accf2cc..061c95401 100755
--- a/src/helpers/geoip-update.py
+++ b/src/helpers/geoip-update.py
@@ -25,20 +25,19 @@ def get_config(config=None):
conf = config
else:
conf = ConfigTreeQuery()
- base = ['firewall']
- if not conf.exists(base):
- return None
-
- return conf.get_config_dict(base, key_mangling=('-', '_'), get_first_key=True,
- no_tag_node_value_mangle=True)
+ return (
+ conf.get_config_dict(['firewall'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True) if conf.exists(['firewall']) else None,
+ conf.get_config_dict(['policy'], key_mangling=('-', '_'), get_first_key=True,
+ no_tag_node_value_mangle=True) if conf.exists(['policy']) else None,
+ )
if __name__ == '__main__':
parser = argparse.ArgumentParser()
parser.add_argument("--force", help="Force update", action="store_true")
args = parser.parse_args()
- firewall = get_config()
-
- if not geoip_update(firewall, force=args.force):
+ firewall, policy = get_config()
+ if not geoip_update(firewall=firewall, policy=policy, force=args.force):
sys.exit(1)
diff --git a/src/helpers/show_commit_data.py b/src/helpers/show_commit_data.py
new file mode 100755
index 000000000..d507ed9a4
--- /dev/null
+++ b/src/helpers/show_commit_data.py
@@ -0,0 +1,56 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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/>.
+#
+#
+# This script is used to show the commit data of the configuration
+
+import sys
+from pathlib import Path
+from argparse import ArgumentParser
+
+from vyos.config_mgmt import ConfigMgmt
+from vyos.configtree import ConfigTree
+from vyos.configtree import show_commit_data
+
+cm = ConfigMgmt()
+
+parser = ArgumentParser(
+ description='Show commit priority queue; no options compares the last two commits'
+)
+parser.add_argument('--active-config', help='Path to the active configuration file')
+parser.add_argument('--proposed-config', help='Path to the proposed configuration file')
+args = parser.parse_args()
+
+active_arg = args.active_config
+proposed_arg = args.proposed_config
+
+if active_arg and not proposed_arg:
+ print('--proposed-config is required when --active-config is specified')
+ sys.exit(1)
+
+if not active_arg and not proposed_arg:
+ active = cm.get_config_tree_revision(1)
+ proposed = cm.get_config_tree_revision(0)
+else:
+ if active_arg:
+ active = ConfigTree(Path(active_arg).read_text())
+ else:
+ active = cm.get_config_tree_revision(0)
+
+ proposed = ConfigTree(Path(proposed_arg).read_text())
+
+ret = show_commit_data(active, proposed)
+print(ret)
diff --git a/src/helpers/test_commit.py b/src/helpers/test_commit.py
new file mode 100755
index 000000000..00a413687
--- /dev/null
+++ b/src/helpers/test_commit.py
@@ -0,0 +1,49 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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/>.
+#
+#
+# This script is used to test execution of the commit algorithm by vyos-commitd
+
+from pathlib import Path
+from argparse import ArgumentParser
+from datetime import datetime
+
+from vyos.configtree import ConfigTree
+from vyos.configtree import test_commit
+
+
+parser = ArgumentParser(
+ description='Execute commit priority queue'
+)
+parser.add_argument(
+ '--active-config', help='Path to the active configuration file', required=True
+)
+parser.add_argument(
+ '--proposed-config', help='Path to the proposed configuration file', required=True
+)
+args = parser.parse_args()
+
+active_arg = args.active_config
+proposed_arg = args.proposed_config
+
+active = ConfigTree(Path(active_arg).read_text())
+proposed = ConfigTree(Path(proposed_arg).read_text())
+
+
+time_begin_commit = datetime.now()
+test_commit(active, proposed)
+time_end_commit = datetime.now()
+print(f'commit time: {time_end_commit - time_begin_commit}')
diff --git a/src/helpers/vyos-certbot-renew-pki.sh b/src/helpers/vyos-certbot-renew-pki.sh
index d0b663f7b..1c273d2fa 100755
--- a/src/helpers/vyos-certbot-renew-pki.sh
+++ b/src/helpers/vyos-certbot-renew-pki.sh
@@ -1,3 +1,3 @@
-#!/bin/sh
+#!/bin/vbash
source /opt/vyatta/etc/functions/script-template
/usr/libexec/vyos/conf_mode/pki.py certbot_renew
diff --git a/src/helpers/vyos-load-balancer.py b/src/helpers/vyos-load-balancer.py
new file mode 100755
index 000000000..30329fd5c
--- /dev/null
+++ b/src/helpers/vyos-load-balancer.py
@@ -0,0 +1,312 @@
+#!/usr/bin/python3
+
+# Copyright 2024-2025 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/>.
+
+import json
+import os
+import signal
+import sys
+import time
+
+from vyos.config import Config
+from vyos.template import render
+from vyos.utils.commit import commit_in_progress
+from vyos.utils.network import get_interface_address
+from vyos.utils.process import rc_cmd
+from vyos.utils.process import run
+from vyos.xml_ref import get_defaults
+from vyos.wanloadbalance import health_ping_host
+from vyos.wanloadbalance import health_ping_host_ttl
+from vyos.wanloadbalance import parse_dhcp_nexthop
+from vyos.wanloadbalance import parse_ppp_nexthop
+
+nftables_wlb_conf = '/run/nftables_wlb.conf'
+wlb_status_file = '/run/wlb_status.json'
+wlb_pid_file = '/run/wlb_daemon.pid'
+sleep_interval = 5 # Main loop sleep interval
+
+def health_check(ifname, conf, state, test_defaults):
+ # Run health tests for interface
+
+ if get_ipv4_address(ifname) is None:
+ return False
+
+ if 'test' not in conf:
+ resp_time = test_defaults['resp-time']
+ target = conf['nexthop']
+
+ if target == 'dhcp':
+ target = state['dhcp_nexthop']
+
+ if not target:
+ return False
+
+ return health_ping_host(target, ifname, wait_time=resp_time)
+
+ for test_id, test_conf in conf['test'].items():
+ check_type = test_conf['type']
+
+ if check_type == 'ping':
+ resp_time = test_conf['resp_time']
+ target = test_conf['target']
+ if not health_ping_host(target, ifname, wait_time=resp_time):
+ return False
+ elif check_type == 'ttl':
+ target = test_conf['target']
+ ttl_limit = test_conf['ttl_limit']
+ if not health_ping_host_ttl(target, ifname, ttl_limit=ttl_limit):
+ return False
+ elif check_type == 'user-defined':
+ script = test_conf['test_script']
+ rc = run(script)
+ if rc != 0:
+ return False
+
+ return True
+
+def on_state_change(lb, ifname, state):
+ # Run hook on state change
+ if 'hook' in lb:
+ script_path = os.path.join('/config/scripts/', lb['hook'])
+ env = {
+ 'WLB_INTERFACE_NAME': ifname,
+ 'WLB_INTERFACE_STATE': 'ACTIVE' if state else 'FAILED'
+ }
+
+ code = run(script_path, env=env)
+ if code != 0:
+ print('WLB hook returned non-zero error code')
+
+ print(f'INFO: State change: {ifname} -> {state}')
+
+def get_ipv4_address(ifname):
+ # Get primary ipv4 address on interface (for source nat)
+ addr_json = get_interface_address(ifname)
+ if addr_json and 'addr_info' in addr_json and len(addr_json['addr_info']) > 0:
+ for addr_info in addr_json['addr_info']:
+ if addr_info['family'] == 'inet':
+ if 'local' in addr_info:
+ return addr_json['addr_info'][0]['local']
+ return None
+
+def dynamic_nexthop_update(lb, ifname):
+ # Update on DHCP/PPP address/nexthop changes
+ # Return True if nftables needs to be updated - IP change
+
+ if 'dhcp_nexthop' in lb['health_state'][ifname]:
+ if ifname[:5] == 'pppoe':
+ dhcp_nexthop_addr = parse_ppp_nexthop(ifname)
+ else:
+ dhcp_nexthop_addr = parse_dhcp_nexthop(ifname)
+
+ table_num = lb['health_state'][ifname]['table_number']
+
+ if dhcp_nexthop_addr and lb['health_state'][ifname]['dhcp_nexthop'] != dhcp_nexthop_addr:
+ lb['health_state'][ifname]['dhcp_nexthop'] = dhcp_nexthop_addr
+ run(f'ip route replace table {table_num} default dev {ifname} via {dhcp_nexthop_addr}')
+
+ if_addr = get_ipv4_address(ifname)
+ if if_addr and if_addr != lb['health_state'][ifname]['if_addr']:
+ lb['health_state'][ifname]['if_addr'] = if_addr
+ return True
+
+ return False
+
+def nftables_update(lb):
+ # Atomically reload nftables table from template
+ if not os.path.exists(nftables_wlb_conf):
+ lb['first_install'] = True
+ elif 'first_install' in lb:
+ del lb['first_install']
+
+ render(nftables_wlb_conf, 'load-balancing/nftables-wlb.j2', lb)
+
+ rc, out = rc_cmd(f'nft -f {nftables_wlb_conf}')
+
+ if rc != 0:
+ print('ERROR: Failed to apply WLB nftables config')
+ print('Output:', out)
+ return False
+
+ return True
+
+def cleanup(lb):
+ if 'interface_health' in lb:
+ index = 1
+ for ifname, health_conf in lb['interface_health'].items():
+ table_num = lb['mark_offset'] + index
+ run(f'ip route del table {table_num} default')
+ run(f'ip rule del fwmark {hex(table_num)} table {table_num}')
+ index += 1
+
+ run(f'nft delete table ip vyos_wanloadbalance')
+
+def get_config():
+ conf = Config()
+ base = ['load-balancing', 'wan']
+ lb = conf.get_config_dict(base, key_mangling=('-', '_'),
+ get_first_key=True, with_recursive_defaults=True)
+
+ lb['test_defaults'] = get_defaults(base + ['interface-health', 'A', 'test', 'B'], get_first_key=True)
+
+ return lb
+
+if __name__ == '__main__':
+ while commit_in_progress():
+ print("Notice: Waiting for commit to complete...")
+ time.sleep(1)
+
+ lb = get_config()
+
+ lb['health_state'] = {}
+ lb['mark_offset'] = 0xc8
+
+ # Create state dicts, interface address and nexthop, install routes and ip rules
+ if 'interface_health' in lb:
+ index = 1
+ for ifname, health_conf in lb['interface_health'].items():
+ table_num = lb['mark_offset'] + index
+ addr = get_ipv4_address(ifname)
+ lb['health_state'][ifname] = {
+ 'if_addr': addr,
+ 'failure_count': 0,
+ 'success_count': 0,
+ 'last_success': 0,
+ 'last_failure': 0,
+ 'state': addr is not None,
+ 'state_changed': False,
+ 'table_number': table_num,
+ 'mark': hex(table_num)
+ }
+
+ if health_conf['nexthop'] == 'dhcp':
+ lb['health_state'][ifname]['dhcp_nexthop'] = None
+
+ dynamic_nexthop_update(lb, ifname)
+ else:
+ run(f'ip route replace table {table_num} default dev {ifname} via {health_conf["nexthop"]}')
+
+ run(f'ip rule add fwmark {hex(table_num)} table {table_num}')
+
+ index += 1
+
+ nftables_update(lb)
+
+ run('ip route flush cache')
+
+ if 'flush_connections' in lb:
+ run('conntrack --delete')
+ run('conntrack -F expect')
+
+ with open(wlb_status_file, 'w') as f:
+ f.write(json.dumps(lb['health_state']))
+
+ # Signal handler SIGUSR2 -> dhcpcd update
+ def handle_sigusr2(signum, frame):
+ for ifname, health_conf in lb['interface_health'].items():
+ if 'nexthop' in health_conf and health_conf['nexthop'] == 'dhcp':
+ retval = dynamic_nexthop_update(lb, ifname)
+
+ if retval:
+ nftables_update(lb)
+
+ # Signal handler SIGTERM -> exit
+ def handle_sigterm(signum, frame):
+ if os.path.exists(wlb_status_file):
+ os.unlink(wlb_status_file)
+
+ if os.path.exists(wlb_pid_file):
+ os.unlink(wlb_pid_file)
+
+ if os.path.exists(nftables_wlb_conf):
+ os.unlink(nftables_wlb_conf)
+
+ cleanup(lb)
+ sys.exit(0)
+
+ signal.signal(signal.SIGUSR2, handle_sigusr2)
+ signal.signal(signal.SIGINT, handle_sigterm)
+ signal.signal(signal.SIGTERM, handle_sigterm)
+
+ with open(wlb_pid_file, 'w') as f:
+ f.write(str(os.getpid()))
+
+ # Main loop
+
+ try:
+ while True:
+ ip_change = False
+
+ if 'interface_health' in lb:
+ for ifname, health_conf in lb['interface_health'].items():
+ state = lb['health_state'][ifname]
+
+ result = health_check(ifname, health_conf, state=state, test_defaults=lb['test_defaults'])
+
+ state_changed = result != state['state']
+ state['state_changed'] = False
+
+ if result:
+ state['failure_count'] = 0
+ state['success_count'] += 1
+ state['last_success'] = time.time()
+ if state_changed and state['success_count'] >= int(health_conf['success_count']):
+ state['state'] = True
+ state['state_changed'] = True
+ elif not result:
+ state['failure_count'] += 1
+ state['success_count'] = 0
+ state['last_failure'] = time.time()
+ if state_changed and state['failure_count'] >= int(health_conf['failure_count']):
+ state['state'] = False
+ state['state_changed'] = True
+
+ if state['state_changed']:
+ state['if_addr'] = get_ipv4_address(ifname)
+ on_state_change(lb, ifname, state['state'])
+
+ if dynamic_nexthop_update(lb, ifname):
+ ip_change = True
+
+ if any(state['state_changed'] for ifname, state in lb['health_state'].items()):
+ if not nftables_update(lb):
+ break
+
+ run('ip route flush cache')
+
+ if 'flush_connections' in lb:
+ run('conntrack --delete')
+ run('conntrack -F expect')
+
+ with open(wlb_status_file, 'w') as f:
+ f.write(json.dumps(lb['health_state']))
+ elif ip_change:
+ nftables_update(lb)
+
+ time.sleep(sleep_interval)
+ except Exception as e:
+ print('WLB ERROR:', e)
+
+ if os.path.exists(wlb_status_file):
+ os.unlink(wlb_status_file)
+
+ if os.path.exists(wlb_pid_file):
+ os.unlink(wlb_pid_file)
+
+ if os.path.exists(nftables_wlb_conf):
+ os.unlink(nftables_wlb_conf)
+
+ cleanup(lb)
diff --git a/src/init/vyos-router b/src/init/vyos-router
index 00136309b..6f1d386d6 100755
--- a/src/init/vyos-router
+++ b/src/init/vyos-router
@@ -459,8 +459,17 @@ start ()
nfct helper add tns inet6 tcp
nft --file /usr/share/vyos/vyos-firewall-init.conf || log_failure_msg "could not initiate firewall rules"
+ # Ensure rsyslog is the default syslog daemon
+ SYSTEMD_SYSLOG="/etc/systemd/system/syslog.service"
+ SYSTEMD_RSYSLOG="/lib/systemd/system/rsyslog.service"
+ if [ ! -L ${SYSTEMD_SYSLOG} ] || [ "$(readlink -f ${SYSTEMD_SYSLOG})" != "${SYSTEMD_RSYSLOG}" ]; then
+ ln -sf ${SYSTEMD_RSYSLOG} ${SYSTEMD_SYSLOG}
+ systemctl daemon-reload
+ fi
+
# As VyOS does not execute commands that are not present in the CLI we call
# the script by hand to have a single source for the login banner and MOTD
+ ${vyos_conf_scripts_dir}/system_syslog.py || log_failure_msg "could not reset syslog"
${vyos_conf_scripts_dir}/system_console.py || log_failure_msg "could not reset serial console"
${vyos_conf_scripts_dir}/system_login_banner.py || log_failure_msg "could not reset motd and issue files"
${vyos_conf_scripts_dir}/system_option.py || log_failure_msg "could not reset system option files"
@@ -556,6 +565,9 @@ start ()
if [[ ! -z "$tmp" ]]; then
vtysh -c "rpki start"
fi
+
+ # Start netplug daemon
+ systemctl start netplug.service
}
stop()
@@ -573,8 +585,8 @@ stop()
umount ${vyatta_configdir}
log_action_end_msg $?
+ systemctl stop netplug.service
systemctl stop vyconfd.service
-
systemctl stop frr.service
unmount_encrypted_config
diff --git a/src/migration-scripts/bgp/5-to-6 b/src/migration-scripts/bgp/5-to-6
new file mode 100644
index 000000000..e6fea6574
--- /dev/null
+++ b/src/migration-scripts/bgp/5-to-6
@@ -0,0 +1,39 @@
+# Copyright 2025 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/>.
+
+# T7163: migrate "address-family ipv4|6-unicast redistribute table" from a multi
+# leafNode to a tagNode. This is needed to support per table definition of a
+# route-map and/or metric
+
+from vyos.configtree import ConfigTree
+
+def migrate(config: ConfigTree) -> None:
+ bgp_base = ['protocols', 'bgp']
+ if not config.exists(bgp_base):
+ return
+
+ for address_family in ['ipv4-unicast', 'ipv6-unicast']:
+ # there is no non-main routing table beeing redistributed under this addres family
+ # bail out early and continue with next AFI
+ table_path = bgp_base + ['address-family', address_family, 'redistribute', 'table']
+ if not config.exists(table_path):
+ continue
+
+ tables = config.return_values(table_path)
+ config.delete(table_path)
+
+ for table in tables:
+ config.set(table_path + [table])
+ config.set_tag(table_path)
diff --git a/src/migration-scripts/dhcp-server/7-to-8 b/src/migration-scripts/dhcp-server/7-to-8
index 7fcb62e86..d0f9455bb 100644
--- a/src/migration-scripts/dhcp-server/7-to-8
+++ b/src/migration-scripts/dhcp-server/7-to-8
@@ -41,9 +41,6 @@ def migrate(config: ConfigTree) -> None:
for network in config.list_nodes(base + ['shared-network-name']):
base_network = base + ['shared-network-name', network]
- if config.exists(base_network + ['ping-check']):
- config.delete(base_network + ['ping-check'])
-
if config.exists(base_network + ['shared-network-parameters']):
config.delete(base_network +['shared-network-parameters'])
@@ -57,9 +54,6 @@ def migrate(config: ConfigTree) -> None:
if config.exists(base_subnet + ['enable-failover']):
config.delete(base_subnet + ['enable-failover'])
- if config.exists(base_subnet + ['ping-check']):
- config.delete(base_subnet + ['ping-check'])
-
if config.exists(base_subnet + ['subnet-parameters']):
config.delete(base_subnet + ['subnet-parameters'])
diff --git a/src/migration-scripts/ids/1-to-2 b/src/migration-scripts/ids/1-to-2
new file mode 100644
index 000000000..4c0333c88
--- /dev/null
+++ b/src/migration-scripts/ids/1-to-2
@@ -0,0 +1,30 @@
+# Copyright 2025 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/>.
+
+# T: Migrate threshold and add new threshold types
+
+from vyos.configtree import ConfigTree
+
+# The old 'service ids' path was only used for FastNetMon
+# Suricata is in 'service suricata',
+# so this isn't an overreach
+base = ['service', 'ids']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ # Nothing to do
+ return
+ else:
+ config.delete(base)
diff --git a/src/migration-scripts/lldp/2-to-3 b/src/migration-scripts/lldp/2-to-3
new file mode 100644
index 000000000..93090756c
--- /dev/null
+++ b/src/migration-scripts/lldp/2-to-3
@@ -0,0 +1,31 @@
+# Copyright 2025 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/>.
+
+# T7165: Migrate LLDP interface disable to 'mode disable'
+
+from vyos.configtree import ConfigTree
+
+base = ['service', 'lldp']
+
+def migrate(config: ConfigTree) -> None:
+ interface_base = base + ['interface']
+ if not config.exists(interface_base):
+ # Nothing to do
+ return
+
+ for interface in config.list_nodes(interface_base):
+ if config.exists(interface_base + [interface, 'disable']):
+ config.delete(interface_base + [interface, 'disable'])
+ config.set(interface_base + [interface, 'mode'], value='disable')
diff --git a/src/migration-scripts/policy/8-to-9 b/src/migration-scripts/policy/8-to-9
new file mode 100644
index 000000000..355e48e00
--- /dev/null
+++ b/src/migration-scripts/policy/8-to-9
@@ -0,0 +1,49 @@
+# Copyright (C) 2025 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/>.
+
+# T7116: Remove unsupported "internet" community following FRR removal
+# From
+ # set policy route-map <name> rule <ord> set community [add | replace] internet
+ # set policy community-list <name> rule <ord> regex internet
+# To
+ # set policy route-map <name> rule <ord> set community [add | replace] 0:0
+ # set policy community-list <name> rule <ord> regex _0:0_
+
+# NOTE: In FRR expanded community-lists, without the '_' delimiters, a regex of
+# "0:0" will match "65000:0" as well as "0:0". This doesn't line up with what
+# we want when replacing "internet".
+
+from vyos.configtree import ConfigTree
+
+rm_base = ['policy', 'route-map']
+cl_base = ['policy', 'community-list']
+
+def migrate(config: ConfigTree) -> None:
+ if config.exists(rm_base):
+ for policy_name in config.list_nodes(rm_base):
+ for rule_ord in config.list_nodes(rm_base + [policy_name, 'rule'], path_must_exist=False):
+ tmp_path = rm_base + [policy_name, 'rule', rule_ord, 'set', 'community']
+ if config.exists(tmp_path + ['add']) and config.return_value(tmp_path + ['add']) == 'internet':
+ config.set(tmp_path + ['add'], '0:0')
+ if config.exists(tmp_path + ['replace']) and config.return_value(tmp_path + ['replace']) == 'internet':
+ config.set(tmp_path + ['replace'], '0:0')
+
+ if config.exists(cl_base):
+ for policy_name in config.list_nodes(cl_base):
+ for rule_ord in config.list_nodes(cl_base + [policy_name, 'rule'], path_must_exist=False):
+ tmp_path = cl_base + [policy_name, 'rule', rule_ord, 'regex']
+ if config.exists(tmp_path) and config.return_value(tmp_path) == 'internet':
+ config.set(tmp_path, '_0:0_')
+
diff --git a/src/migration-scripts/reverse-proxy/2-to-3 b/src/migration-scripts/reverse-proxy/2-to-3
new file mode 100755
index 000000000..ac539618e
--- /dev/null
+++ b/src/migration-scripts/reverse-proxy/2-to-3
@@ -0,0 +1,66 @@
+# Copyright 2025 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/>.
+
+# T7429: logging facility "all" unavailable in code
+
+from vyos.configtree import ConfigTree
+
+base = ['load-balancing', 'haproxy']
+unsupported_facilities = ['all', 'authpriv', 'mark']
+
+def config_migrator(config, config_path: list) -> None:
+ if not config.exists(config_path):
+ return
+ # Remove unsupported backend HAProxy syslog facilities form CLI
+ # Works for both backend and service CLI nodes
+ for service_backend in config.list_nodes(config_path):
+ log_path = config_path + [service_backend, 'logging', 'facility']
+ if not config.exists(log_path):
+ continue
+ # Remove unsupported syslog facilities form CLI
+ for facility in config.list_nodes(log_path):
+ if facility in unsupported_facilities:
+ config.delete(log_path + [facility])
+ continue
+ # Remove unsupported facility log level form CLI. VyOS will fallback
+ # to default log level if not set
+ if config.exists(log_path + [facility, 'level']):
+ tmp = config.return_value(log_path + [facility, 'level'])
+ if tmp == 'all':
+ config.delete(log_path + [facility, 'level'])
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ # Nothing to do
+ return
+
+ # Remove unsupported syslog facilities form CLI
+ global_path = base + ['global-parameters', 'logging', 'facility']
+ if config.exists(global_path):
+ for facility in config.list_nodes(global_path):
+ if facility in unsupported_facilities:
+ config.delete(global_path + [facility])
+ continue
+ # Remove unsupported facility log level form CLI. VyOS will fallback
+ # to default log level if not set
+ if config.exists(global_path + [facility, 'level']):
+ tmp = config.return_value(global_path + [facility, 'level'])
+ if tmp == 'all':
+ config.delete(global_path + [facility, 'level'])
+
+ # Remove unsupported backend HAProxy syslog facilities from CLI
+ config_migrator(config, base + ['backend'])
+ # Remove unsupported service HAProxy syslog facilities from CLI
+ config_migrator(config, base + ['service'])
diff --git a/src/migration-scripts/system/28-to-29 b/src/migration-scripts/system/28-to-29
new file mode 100644
index 000000000..ccf7056c4
--- /dev/null
+++ b/src/migration-scripts/system/28-to-29
@@ -0,0 +1,71 @@
+# Copyright 2025 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/>.
+
+# T6989:
+# - remove syslog arbitrary file logging
+# - remove syslog user console logging
+# - move "global preserve-fqdn" one CLI level up
+# - rename "host" to "remote"
+
+from vyos.configtree import ConfigTree
+
+base = ['system', 'syslog']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ return
+ # Drop support for custom file logging
+ if config.exists(base + ['file']):
+ config.delete(base + ['file'])
+
+ # Drop support for logging to a user tty
+ # This should be dynamically added via an op-mode command like "terminal monitor"
+ if config.exists(base + ['user']):
+ config.delete(base + ['user'])
+
+ # Move "global preserve-fqdn" one CLI level up, as it relates to all
+ # logging targets (console, global and remote)
+ preserve_fqdn_base = base + ['global', 'preserve-fqdn']
+ if config.exists(preserve_fqdn_base):
+ config.delete(preserve_fqdn_base)
+ config.set(base + ['preserve-fqdn'])
+
+ # Move "global marker" one CLI level up, as it relates to all
+ # logging targets (console, global and remote)
+ marker_base = base + ['global', 'marker']
+ if config.exists(marker_base):
+ config.copy(marker_base, base + ['marker'])
+ config.delete(marker_base)
+
+ # Rename "global" -> "local" as this describes what is logged locally
+ # on the router to a file on the filesystem
+ if config.exists(base + ['global']):
+ config.rename(base + ['global'], 'local')
+
+ vrf = ''
+ if config.exists(base + ['vrf']):
+ vrf = config.return_value(base + ['vrf'])
+ config.delete(base + ['vrf'])
+
+ # Rename host x.x.x.x -> remote x.x.x.x
+ if config.exists(base + ['host']):
+ config.set(base + ['remote'])
+ config.set_tag(base + ['remote'])
+ for remote in config.list_nodes(base + ['host']):
+ config.copy(base + ['host', remote], base + ['remote', remote])
+ config.set_tag(base + ['remote'])
+ if vrf:
+ config.set(base + ['remote', remote, 'vrf'], value=vrf)
+ config.delete(base + ['host'])
diff --git a/src/migration-scripts/vrf/1-to-2 b/src/migration-scripts/vrf/1-to-2
index 557a9ec58..89b0f708a 100644
--- a/src/migration-scripts/vrf/1-to-2
+++ b/src/migration-scripts/vrf/1-to-2
@@ -37,7 +37,10 @@ def migrate(config: ConfigTree) -> None:
new_static_base = vrf_base + [vrf, 'protocols']
config.set(new_static_base)
config.copy(static_base, new_static_base + ['static'])
- config.set_tag(new_static_base + ['static', 'route'])
+ if config.exists(new_static_base + ['static', 'route']):
+ config.set_tag(new_static_base + ['static', 'route'])
+ if config.exists(new_static_base + ['static', 'route6']):
+ config.set_tag(new_static_base + ['static', 'route6'])
# Now delete the old configuration
config.delete(base)
diff --git a/src/migration-scripts/vrf/2-to-3 b/src/migration-scripts/vrf/2-to-3
index acacffb41..5f396e7ed 100644
--- a/src/migration-scripts/vrf/2-to-3
+++ b/src/migration-scripts/vrf/2-to-3
@@ -76,7 +76,8 @@ def migrate(config: ConfigTree) -> None:
# Get a list of all currently used VRFs and tables
vrfs_current = {}
for vrf in config.list_nodes(base):
- vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table']))
+ if config.exists(base + [vrf, 'table']):
+ vrfs_current[vrf] = int(config.return_value(base + [vrf, 'table']))
# Check VRF names and table numbers
name_regex = re.compile(r'^\d.*$')
diff --git a/src/migration-scripts/wanloadbalance/3-to-4 b/src/migration-scripts/wanloadbalance/3-to-4
new file mode 100644
index 000000000..e49f46a5b
--- /dev/null
+++ b/src/migration-scripts/wanloadbalance/3-to-4
@@ -0,0 +1,33 @@
+# Copyright 2025 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.configtree import ConfigTree
+
+base = ['load-balancing', 'wan']
+
+def migrate(config: ConfigTree) -> None:
+ if not config.exists(base):
+ # Nothing to do
+ return
+
+ if config.exists(base + ['rule']):
+ for rule in config.list_nodes(base + ['rule']):
+ rule_base = base + ['rule', rule]
+
+ if config.exists(rule_base + ['inbound-interface']):
+ ifname = config.return_value(rule_base + ['inbound-interface'])
+
+ if ifname.endswith('+'):
+ config.set(rule_base + ['inbound-interface'], value=ifname.replace('+', '*'))
diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py
index 8eed2c6cd..725bfc75b 100755
--- a/src/op_mode/dhcp.py
+++ b/src/op_mode/dhcp.py
@@ -205,7 +205,7 @@ def _get_raw_server_pool_statistics(config, family='inet', pool=None):
return stats
-def _get_formatted_server_pool_statistics(pool_data, family='inet'):
+def _get_formatted_server_pool_statistics(pool_data):
data_entries = []
for entry in pool_data:
pool = entry.get('pool')
@@ -235,7 +235,7 @@ def _get_raw_server_static_mappings(config, family='inet', pool=None, sorted=Non
return mappings
-def _get_formatted_server_static_mappings(raw_data, family='inet'):
+def _get_formatted_server_static_mappings(raw_data):
data_entries = []
for entry in raw_data:
@@ -245,10 +245,8 @@ def _get_formatted_server_static_mappings(raw_data, family='inet'):
ip_addr = entry.get('ip', 'N/A')
mac_addr = entry.get('mac', 'N/A')
duid = entry.get('duid', 'N/A')
- description = entry.get('description', 'N/A')
- data_entries.append(
- [pool, subnet, hostname, ip_addr, mac_addr, duid, description]
- )
+ desc = entry.get('description', 'N/A')
+ data_entries.append([pool, subnet, hostname, ip_addr, mac_addr, duid, desc])
headers = [
'Pool',
@@ -327,7 +325,7 @@ def show_server_pool_statistics(
if raw:
return pool_data
else:
- return _get_formatted_server_pool_statistics(pool_data, family=family)
+ return _get_formatted_server_pool_statistics(pool_data)
@_verify_server
@@ -408,7 +406,7 @@ def show_server_static_mappings(
if raw:
return static_mappings
else:
- return _get_formatted_server_static_mappings(static_mappings, family=family)
+ return _get_formatted_server_static_mappings(static_mappings)
def _lease_valid(inet, address):
@@ -482,7 +480,7 @@ def _get_raw_client_leases(family='inet', interface=None):
return lease_data
-def _get_formatted_client_leases(lease_data, family):
+def _get_formatted_client_leases(lease_data):
from time import localtime
from time import strftime
@@ -534,7 +532,7 @@ def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[
if raw:
return lease_data
else:
- return _get_formatted_client_leases(lease_data, family=family)
+ return _get_formatted_client_leases(lease_data)
@_verify_client
diff --git a/src/op_mode/firewall.py b/src/op_mode/firewall.py
index c197ca434..f3309ee34 100755
--- a/src/op_mode/firewall.py
+++ b/src/op_mode/firewall.py
@@ -18,6 +18,7 @@ import argparse
import ipaddress
import json
import re
+from signal import signal, SIGPIPE, SIG_DFL
import tabulate
import textwrap
@@ -25,6 +26,9 @@ from vyos.config import Config
from vyos.utils.process import cmd
from vyos.utils.dict import dict_search_args
+signal(SIGPIPE, SIG_DFL)
+
+
def get_config_node(conf, node=None, family=None, hook=None, priority=None):
if node == 'nat':
if family == 'ipv6':
@@ -148,6 +152,38 @@ def get_nftables_group_members(family, table, name):
return out
+def get_nftables_remote_group_members(family, table, name):
+ prefix = 'ip6' if family == 'ipv6' else 'ip'
+ out = []
+
+ try:
+ results_str = cmd(f'nft -j list set {prefix} {table} {name}')
+ results = json.loads(results_str)
+ except:
+ return out
+
+ if 'nftables' not in results:
+ return out
+
+ for obj in results['nftables']:
+ if 'set' not in obj:
+ continue
+
+ set_obj = obj['set']
+ if 'elem' in set_obj:
+ for elem in set_obj['elem']:
+ # search for single IP elements
+ if isinstance(elem, str):
+ out.append(elem)
+ # search for prefix elements
+ elif isinstance(elem, dict) and 'prefix' in elem:
+ out.append(f"{elem['prefix']['addr']}/{elem['prefix']['len']}")
+ # search for IP range elements
+ elif isinstance(elem, dict) and 'range' in elem:
+ out.append(f"{elem['range'][0]}-{elem['range'][1]}")
+
+ return out
+
def output_firewall_vertical(rules, headers, adjust=True):
for rule in rules:
adjusted_rule = rule + [""] * (len(headers) - len(rule)) if adjust else rule # account for different header length, like default-action
@@ -253,15 +289,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
if not source_addr:
source_addr = dict_search_args(rule_conf, 'source', 'group', 'domain_group')
if not source_addr:
- source_addr = dict_search_args(rule_conf, 'source', 'fqdn')
+ source_addr = dict_search_args(rule_conf, 'source', 'group', 'remote_group')
if not source_addr:
- source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code')
- if source_addr:
- source_addr = str(source_addr)[1:-1].replace('\'','')
- if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'):
- source_addr = 'NOT ' + str(source_addr)
+ source_addr = dict_search_args(rule_conf, 'source', 'fqdn')
if not source_addr:
- source_addr = 'any'
+ source_addr = dict_search_args(rule_conf, 'source', 'geoip', 'country_code')
+ if source_addr:
+ source_addr = str(source_addr)[1:-1].replace('\'','')
+ if 'inverse_match' in dict_search_args(rule_conf, 'source', 'geoip'):
+ source_addr = 'NOT ' + str(source_addr)
+ if not source_addr:
+ source_addr = 'any'
# Get destination
dest_addr = dict_search_args(rule_conf, 'destination', 'address')
@@ -272,15 +310,17 @@ def output_firewall_name_statistics(family, hook, prior, prior_conf, single_rule
if not dest_addr:
dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'domain_group')
if not dest_addr:
- dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn')
+ dest_addr = dict_search_args(rule_conf, 'destination', 'group', 'remote_group')
if not dest_addr:
- dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code')
- if dest_addr:
- dest_addr = str(dest_addr)[1:-1].replace('\'','')
- if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'):
- dest_addr = 'NOT ' + str(dest_addr)
+ dest_addr = dict_search_args(rule_conf, 'destination', 'fqdn')
if not dest_addr:
- dest_addr = 'any'
+ dest_addr = dict_search_args(rule_conf, 'destination', 'geoip', 'country_code')
+ if dest_addr:
+ dest_addr = str(dest_addr)[1:-1].replace('\'','')
+ if 'inverse_match' in dict_search_args(rule_conf, 'destination', 'geoip'):
+ dest_addr = 'NOT ' + str(dest_addr)
+ if not dest_addr:
+ dest_addr = 'any'
# Get inbound interface
iiface = dict_search_args(rule_conf, 'inbound_interface', 'name')
@@ -552,30 +592,8 @@ def show_firewall_group(name=None):
header_tail = []
for group_type, group_type_conf in firewall['group'].items():
- ##
- if group_type != 'dynamic_group':
-
- 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, textwrap.fill(group_conf.get('description') or '', 50), 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:
+ # interate over dynamic-groups
+ if group_type == 'dynamic_group':
if not args.detail:
header_tail = ['Timeout', 'Expires']
@@ -584,6 +602,9 @@ def show_firewall_group(name=None):
prefix = 'DA_' if dynamic_type == 'address_group' else 'DA6_'
if dynamic_type in firewall['group']['dynamic_group']:
for dynamic_name, dynamic_conf in firewall['group']['dynamic_group'][dynamic_type].items():
+ if name and name != dynamic_name:
+ continue
+
references = find_references(dynamic_type, dynamic_name)
row = [dynamic_name, textwrap.fill(dynamic_conf.get('description') or '', 50), dynamic_type + '(dynamic)', '\n'.join(references) or 'N/D']
@@ -622,6 +643,68 @@ def show_firewall_group(name=None):
header_tail += [""] * (len(members) - 1)
rows.append(row)
+ # iterate over remote-groups
+ elif group_type == 'remote_group':
+ for remote_name, remote_conf in group_type_conf.items():
+ if name and name != remote_name:
+ continue
+
+ references = find_references(group_type, remote_name)
+ row = [remote_name, textwrap.fill(remote_conf.get('description') or '', 50), group_type, '\n'.join(references) or 'N/D']
+ members = get_nftables_remote_group_members("ipv4", 'vyos_filter', f'R_{remote_name}')
+ members6 = get_nftables_remote_group_members("ipv6", 'vyos_filter', f'R6_{remote_name}')
+
+ if 'url' in remote_conf:
+ # display only the url if no members are found for both views
+ if not members and not members6:
+ if args.detail:
+ header_tail = ['IPv6 Members', 'Remote URL']
+ row.append('N/D')
+ row.append('N/D')
+ row.append(remote_conf['url'])
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ # display all table elements in detail view
+ if args.detail:
+ header_tail = ['IPv6 Members', 'Remote URL']
+ if members:
+ row.append(' '.join(members))
+ else:
+ row.append('N/D')
+ if members6:
+ row.append(' '.join(members6))
+ else:
+ row.append('N/D')
+ row.append(remote_conf['url'])
+ rows.append(row)
+ else:
+ row.append(remote_conf['url'])
+ rows.append(row)
+
+ # catch the rest of the group types
+ else:
+ 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, textwrap.fill(group_conf.get('description') or '', 50), 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)
+
if rows:
print('Firewall Groups\n')
if args.detail:
diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py
index 1da112673..ac5a84419 100755
--- a/src/op_mode/image_installer.py
+++ b/src/op_mode/image_installer.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright 2023-2024 VyOS maintainers and contributors <maintainers@vyos.io>
+# Copyright 2023-2025 VyOS maintainers and contributors <maintainers@vyos.io>
#
# This file is part of VyOS.
#
@@ -24,7 +24,9 @@ from glob import glob
from sys import exit
from os import environ
from os import readlink
-from os import getpid, getppid
+from os import getpid
+from os import getppid
+from json import loads
from typing import Union
from urllib.parse import urlparse
from passlib.hosts import linux_context
@@ -32,12 +34,26 @@ from errno import ENOSPC
from psutil import disk_partitions
+from vyos.base import Warning
from vyos.configtree import ConfigTree
from vyos.remote import download
-from vyos.system import disk, grub, image, compat, raid, SYSTEM_CFG_VER
+from vyos.system import disk
+from vyos.system import grub
+from vyos.system import image
+from vyos.system import compat
+from vyos.system import raid
+from vyos.system import SYSTEM_CFG_VER
+from vyos.system import grub_util
from vyos.template import render
+from vyos.utils.auth import (
+ DEFAULT_PASSWORD,
+ EPasswdStrength,
+ evaluate_strength
+)
+from vyos.utils.dict import dict_search
from vyos.utils.io import ask_input, ask_yes_no, select_entry
from vyos.utils.file import chmod_2775
+from vyos.utils.file import read_file
from vyos.utils.process import cmd, run, rc_cmd
from vyos.version import get_version_data
@@ -46,7 +62,13 @@ MSG_ERR_NOT_LIVE: str = 'The system is already installed. Please use "add system
MSG_ERR_LIVE: str = 'The system is in live-boot mode. Please use "install image" instead.'
MSG_ERR_NO_DISK: str = 'No suitable disk was found. There must be at least one disk of 2GB or greater size.'
MSG_ERR_IMPROPER_IMAGE: str = 'Missing sha256sum.txt.\nEither this image is corrupted, or of era 1.2.x (md5sum) and would downgrade image tools;\ndisallowed in either case.'
-MSG_ERR_ARCHITECTURE_MISMATCH: str = 'Upgrading to a different image architecture will break your system.'
+MSG_ERR_INCOMPATIBLE_IMAGE: str = 'Image compatibility check failed, aborting installation.'
+MSG_ERR_ARCHITECTURE_MISMATCH: str = 'The current architecture is "{0}", the new image is for "{1}". Upgrading to a different image architecture will break your system.'
+MSG_ERR_FLAVOR_MISMATCH: str = 'The current image flavor is "{0}", the new image is "{1}". Upgrading to a non-matching flavor can have unpredictable consequences.'
+MSG_ERR_MISSING_ARCHITECTURE: str = 'The new image version data does not specify architecture, cannot check compatibility (is it a legacy release image?)'
+MSG_ERR_MISSING_FLAVOR: str = 'The new image version data does not specify flavor, cannot check compatibility (is it a legacy release image?)'
+MSG_ERR_CORRUPT_CURRENT_IMAGE: str = 'Version data in the current image is malformed: missing flavor and/or architecture fields. Upgrade compatibility cannot be checked.'
+MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE: str = 'Unsupported signature type, signature cannot be verified.'
MSG_INFO_INSTALL_WELCOME: str = 'Welcome to VyOS installation!\nThis command will install VyOS to your permanent storage.'
MSG_INFO_INSTALL_EXIT: str = 'Exiting from VyOS installation'
MSG_INFO_INSTALL_SUCCESS: str = 'The image installed successfully; please reboot now.'
@@ -62,6 +84,7 @@ MSG_INPUT_CONFIG_FOUND: str = 'An active configuration was found. Would you like
MSG_INPUT_CONFIG_CHOICE: str = 'The following config files are available for boot:'
MSG_INPUT_CONFIG_CHOOSE: str = 'Which file would you like as boot config?'
MSG_INPUT_IMAGE_NAME: str = 'What would you like to name this image?'
+MSG_INPUT_IMAGE_NAME_TAKEN: str = 'There is already an installed image by that name; please choose again'
MSG_INPUT_IMAGE_DEFAULT: str = 'Would you like to set the new image as the default one for boot?'
MSG_INPUT_PASSWORD: str = 'Please enter a password for the "vyos" user:'
MSG_INPUT_PASSWORD_CONFIRM: str = 'Please confirm password for the "vyos" user:'
@@ -78,8 +101,10 @@ 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 64 characters long and contains only the next characters: .+-_ a-z A-Z 0-9'
+
+MSG_WARN_CHANGE_PASSWORD: str = 'Default password used. Consider changing ' \
+ 'it on next login.'
MSG_WARN_PASSWORD_CONFIRM: str = 'The entered values did not match. Try again'
-MSG_WARN_FLAVOR_MISMATCH: str = 'The running image flavor is "{0}". The new image flavor is "{1}".\n' \
'Installing a different image flavor may cause functionality degradation or break your system.\n' \
'Do you want to continue with installation?'
CONST_MIN_DISK_SIZE: int = 2147483648 # 2 GB
@@ -95,7 +120,7 @@ DIR_ISO_MOUNT: str = f'{DIR_INSTALLATION}/iso_src'
DIR_DST_ROOT: str = f'{DIR_INSTALLATION}/disk_dst'
DIR_KERNEL_SRC: str = '/boot/'
FILE_ROOTFS_SRC: str = '/usr/lib/live/mount/medium/live/filesystem.squashfs'
-ISO_DOWNLOAD_PATH: str = '/tmp/vyos_installation.iso'
+ISO_DOWNLOAD_PATH: str = ''
external_download_script = '/usr/libexec/vyos/simple-download.py'
external_latest_image_url_script = '/usr/libexec/vyos/latest-image-url.py'
@@ -462,6 +487,29 @@ def setup_grub(root_dir: str) -> None:
render(grub_cfg_menu, grub.TMPL_GRUB_MENU, {})
render(grub_cfg_options, grub.TMPL_GRUB_OPTS, {})
+def get_cli_kernel_options(config_file: str) -> list:
+ config = ConfigTree(read_file(config_file))
+ config_dict = loads(config.to_json())
+ kernel_options = dict_search('system.option.kernel', config_dict)
+ if kernel_options is None:
+ kernel_options = {}
+ cmdline_options = []
+
+ # XXX: This code path and if statements must be kept in sync with the Kernel
+ # option handling in system_options.py:generate(). This occurance is used
+ # for having the appropriate options passed to GRUB after an image upgrade!
+ if 'disable-mitigations' in kernel_options:
+ cmdline_options.append('mitigations=off')
+ if 'disable-power-saving' in kernel_options:
+ cmdline_options.append('intel_idle.max_cstate=0 processor.max_cstate=1')
+ if 'amd-pstate-driver' in kernel_options:
+ mode = kernel_options['amd-pstate-driver']
+ cmdline_options.append(
+ f'initcall_blacklist=acpi_cpufreq_init amd_pstate={mode}')
+ if 'quiet' in kernel_options:
+ cmdline_options.append('quiet')
+
+ return cmdline_options
def configure_authentication(config_file: str, password: str) -> None:
"""Write encrypted password to config file
@@ -476,10 +524,7 @@ def configure_authentication(config_file: str, password: str) -> None:
plaintext exposed
"""
encrypted_password = linux_context.hash(password)
-
- with open(config_file) as f:
- config_string = f.read()
-
+ config_string = read_file(config_file)
config = ConfigTree(config_string)
config.set([
'system', 'login', 'user', 'vyos', 'authentication',
@@ -501,7 +546,6 @@ def validate_signature(file_path: str, sign_type: str) -> None:
"""
print('Validating signature')
signature_valid: bool = False
- # validate with minisig
if sign_type == 'minisig':
pub_key_list = glob('/usr/share/vyos/keys/*.minisign.pub')
for pubkey in pub_key_list:
@@ -510,11 +554,8 @@ def validate_signature(file_path: str, sign_type: str) -> None:
signature_valid = True
break
Path(f'{file_path}.minisig').unlink()
- # validate with GPG
- if sign_type == 'asc':
- if run(f'gpg --verify ${file_path}.asc ${file_path}') == 0:
- signature_valid = True
- Path(f'{file_path}.asc').unlink()
+ else:
+ exit(MSG_ERR_UNSUPPORTED_SIGNATURE_TYPE)
# warn or pass
if not signature_valid:
@@ -524,21 +565,18 @@ def validate_signature(file_path: str, sign_type: str) -> None:
print('Signature is valid')
def download_file(local_file: str, remote_path: str, vrf: str,
- username: str, password: str,
progressbar: bool = False, check_space: bool = False):
- environ['REMOTE_USERNAME'] = username
- environ['REMOTE_PASSWORD'] = password
+ # Server credentials are implicitly passed in environment variables
+ # that are set by add_image
if vrf is None:
download(local_file, remote_path, progressbar=progressbar,
check_space=check_space, raise_error=True)
else:
- remote_auth = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password}'
vrf_cmd = f'ip vrf exec {vrf} {external_download_script} \
--local-file {local_file} --remote-path {remote_path}'
- cmd(vrf_cmd, auth=remote_auth)
+ cmd(vrf_cmd, env=environ)
def image_fetch(image_path: str, vrf: str = None,
- username: str = '', password: str = '',
no_prompt: bool = False) -> Path:
"""Fetch an ISO image
@@ -548,13 +586,17 @@ def image_fetch(image_path: str, vrf: str = None,
Returns:
Path: a path to a local file
"""
+ import os.path
+ from uuid import uuid4
+
+ global ISO_DOWNLOAD_PATH
+
# Latest version gets url from configured "system update-check url"
if image_path == 'latest':
command = external_latest_image_url_script
if vrf:
- command = f'REMOTE_USERNAME={username} REMOTE_PASSWORD={password} \
- ip vrf exec {vrf} ' + command
- code, output = rc_cmd(command)
+ command = f'ip vrf exec {vrf} {command}'
+ code, output = rc_cmd(command, env=environ)
if code:
print(output)
exit(MSG_INFO_INSTALL_EXIT)
@@ -563,23 +605,25 @@ def image_fetch(image_path: str, vrf: str = None,
try:
# check a type of path
if urlparse(image_path).scheme:
- # download an image
+ # Download the image file
+ ISO_DOWNLOAD_PATH = os.path.join(os.path.expanduser("~"), '{0}.iso'.format(uuid4()))
download_file(ISO_DOWNLOAD_PATH, image_path, vrf,
- username, password,
progressbar=True, check_space=True)
- # download a signature
+ # Download the image signature
+ # VyOS only supports minisign signatures at the moment,
+ # but we keep the logic for multiple signatures
+ # in case we add something new in the future
sign_file = (False, '')
- for sign_type in ['minisig', 'asc']:
+ for sign_type in ['minisig']:
try:
download_file(f'{ISO_DOWNLOAD_PATH}.{sign_type}',
- f'{image_path}.{sign_type}', vrf,
- username, password)
+ f'{image_path}.{sign_type}', vrf)
sign_file = (True, sign_type)
break
except Exception:
- print(f'{sign_type} signature is not available')
- # validate a signature if it is available
+ print(f'Could not download {sign_type} signature')
+ # Validate the signature if it is available
if sign_file[0]:
validate_signature(ISO_DOWNLOAD_PATH, sign_file[1])
else:
@@ -701,30 +745,48 @@ def is_raid_install(install_object: Union[disk.DiskDetails, raid.RaidDetails]) -
return False
-def validate_compatibility(iso_path: str) -> None:
+def validate_compatibility(iso_path: str, force: bool = False) -> None:
"""Check architecture and flavor compatibility with the running image
Args:
iso_path (str): a path to the mounted ISO image
"""
- old_data = get_version_data()
- old_flavor = old_data.get('flavor', '')
- old_architecture = old_data.get('architecture') or cmd('dpkg --print-architecture')
+ current_data = get_version_data()
+ current_flavor = current_data.get('flavor')
+ current_architecture = current_data.get('architecture') or cmd('dpkg --print-architecture')
new_data = get_version_data(f'{iso_path}/version.json')
- new_flavor = new_data.get('flavor', '')
- new_architecture = new_data.get('architecture', '')
+ new_flavor = new_data.get('flavor')
+ new_architecture = new_data.get('architecture')
- if not old_architecture == new_architecture:
- print(MSG_ERR_ARCHITECTURE_MISMATCH)
+ if not current_flavor or not current_architecture:
+ # This may only happen if someone modified the version file.
+ # Unlikely but not impossible.
+ print(MSG_ERR_CORRUPT_CURRENT_IMAGE)
cleanup()
exit(MSG_INFO_INSTALL_EXIT)
- if not old_flavor == new_flavor:
- if not ask_yes_no(MSG_WARN_FLAVOR_MISMATCH.format(old_flavor, new_flavor), default=False):
- cleanup()
- exit(MSG_INFO_INSTALL_EXIT)
+ success = True
+
+ if current_architecture != new_architecture:
+ success = False
+ if not new_architecture:
+ print(MSG_ERR_MISSING_ARCHITECTURE)
+ else:
+ print(MSG_ERR_ARCHITECTURE_MISMATCH.format(current_architecture, new_architecture))
+
+ if current_flavor != new_flavor:
+ if not force:
+ success = False
+ if not new_flavor:
+ print(MSG_ERR_MISSING_FLAVOR)
+ else:
+ print(MSG_ERR_FLAVOR_MISMATCH.format(current_flavor, new_flavor))
+ if not success:
+ print(MSG_ERR_INCOMPATIBLE_IMAGE)
+ cleanup()
+ exit(MSG_INFO_INSTALL_EXIT)
def install_image() -> None:
"""Install an image to a disk
@@ -746,14 +808,25 @@ def install_image() -> None:
break
print(MSG_WARN_IMAGE_NAME_WRONG)
+ failed_check_status = [EPasswdStrength.WEAK, EPasswdStrength.ERROR]
# ask for password
while True:
user_password: str = ask_input(MSG_INPUT_PASSWORD, no_echo=True,
non_empty=True)
+
+ if user_password == DEFAULT_PASSWORD:
+ Warning(MSG_WARN_CHANGE_PASSWORD)
+ else:
+ result = evaluate_strength(user_password)
+ if result['strength'] in failed_check_status:
+ Warning(result['error'])
+
confirm: str = ask_input(MSG_INPUT_PASSWORD_CONFIRM, no_echo=True,
non_empty=True)
+
if user_password == confirm:
break
+
print(MSG_WARN_PASSWORD_CONFIRM)
# ask for default console
@@ -849,8 +922,7 @@ def install_image() -> None:
for disk_target in l:
disk.partition_mount(disk_target.partition['efi'], f'{DIR_DST_ROOT}/boot/efi')
grub.install(disk_target.name, f'{DIR_DST_ROOT}/boot/',
- f'{DIR_DST_ROOT}/boot/efi',
- id=f'VyOS (RAID disk {l.index(disk_target) + 1})')
+ f'{DIR_DST_ROOT}/boot/efi')
disk.partition_umount(disk_target.partition['efi'])
else:
print('Installing GRUB to the drive')
@@ -893,7 +965,7 @@ def install_image() -> None:
@compat.grub_cfg_update
def add_image(image_path: str, vrf: str = None, username: str = '',
- password: str = '', no_prompt: bool = False) -> None:
+ password: str = '', no_prompt: bool = False, force: bool = False) -> None:
"""Add a new image
Args:
@@ -902,15 +974,18 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
if image.is_live_boot():
exit(MSG_ERR_LIVE)
+ environ['REMOTE_USERNAME'] = username
+ environ['REMOTE_PASSWORD'] = password
+
# fetch an image
- iso_path: Path = image_fetch(image_path, vrf, username, password, no_prompt)
+ iso_path: Path = image_fetch(image_path, vrf, no_prompt)
try:
# mount an ISO
Path(DIR_ISO_MOUNT).mkdir(mode=0o755, parents=True)
disk.partition_mount(iso_path, DIR_ISO_MOUNT, 'iso9660')
print('Validating image compatibility')
- validate_compatibility(DIR_ISO_MOUNT)
+ validate_compatibility(DIR_ISO_MOUNT, force=force)
# check sums
print('Validating image checksums')
@@ -936,8 +1011,12 @@ 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:
+ versions = grub.version_list()
while True:
image_name: str = ask_input(MSG_INPUT_IMAGE_NAME, version_name)
+ if image_name in versions:
+ print(MSG_INPUT_IMAGE_NAME_TAKEN)
+ continue
if image.validate_name(image_name):
break
print(MSG_WARN_IMAGE_NAME_WRONG)
@@ -959,7 +1038,7 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
Path(target_config_dir).mkdir(parents=True)
chown(target_config_dir, group='vyattacfg')
chmod_2775(target_config_dir)
- copytree('/opt/vyatta/etc/config/', target_config_dir,
+ copytree('/opt/vyatta/etc/config/', target_config_dir, symlinks=True,
copy_function=copy_preserve_owner, dirs_exist_ok=True)
else:
Path(target_config_dir).mkdir(parents=True)
@@ -992,6 +1071,12 @@ def add_image(image_path: str, vrf: str = None, username: str = '',
if set_as_default:
grub.set_default(image_name, root_dir)
+ cmdline_options = get_cli_kernel_options(
+ f'{target_config_dir}/config.boot')
+ grub_util.update_kernel_cmdline_options(' '.join(cmdline_options),
+ root_dir=root_dir,
+ version=image_name)
+
except OSError as e:
# if no space error, remove image dir and cleanup
if e.errno == ENOSPC:
@@ -1031,6 +1116,9 @@ def parse_arguments() -> Namespace:
parser.add_argument('--image-path',
help='a path (HTTP or local file) to an image that needs to be installed'
)
+ parser.add_argument('--force', action='store_true',
+ help='Ignore flavor compatibility requirements.'
+ )
# parser.add_argument('--image_new_name', help='a new name for image')
args: Namespace = parser.parse_args()
# Validate arguments
@@ -1047,7 +1135,8 @@ if __name__ == '__main__':
install_image()
if args.action == 'add':
add_image(args.image_path, args.vrf,
- args.username, args.password, args.no_prompt)
+ args.username, args.password,
+ args.no_prompt, args.force)
exit()
diff --git a/src/op_mode/interfaces.py b/src/op_mode/interfaces.py
index e7afc4caa..c97f3b129 100755
--- a/src/op_mode/interfaces.py
+++ b/src/op_mode/interfaces.py
@@ -29,6 +29,7 @@ from vyos.ifconfig import Section
from vyos.ifconfig import Interface
from vyos.ifconfig import VRRP
from vyos.utils.process import cmd
+from vyos.utils.network import interface_exists
from vyos.utils.process import rc_cmd
from vyos.utils.process import call
@@ -84,6 +85,14 @@ def filtered_interfaces(ifnames: typing.Union[str, list],
yield interface
+def detailed_output(dataset, headers):
+ for data in dataset:
+ adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action
+ transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char
+
+ print(tabulate(transformed_rule, tablefmt="presto"))
+ print()
+
def _split_text(text, used=0):
"""
take a string and attempt to split it to fit with the width of the screen
@@ -296,6 +305,114 @@ def _get_counter_data(ifname: typing.Optional[str],
return ret
+def _get_kernel_data(raw, ifname = None, detail = False):
+ if ifname:
+ # Check if the interface exists
+ if not interface_exists(ifname):
+ raise vyos.opmode.IncorrectValue(f"{ifname} does not exist!")
+ int_name = f'dev {ifname}'
+ else:
+ int_name = ''
+
+ kernel_interface = json.loads(cmd(f'ip -j -d -s address show {int_name}'))
+
+ # Return early if raw
+ if raw:
+ return kernel_interface, None
+
+ # Format the kernel data
+ kernel_interface_out = _format_kernel_data(kernel_interface, detail)
+
+ return kernel_interface, kernel_interface_out
+
+def _format_kernel_data(data, detail):
+ output_list = []
+ tmpInfo = {}
+
+ # Sort interfaces by name
+ for interface in sorted(data, key=lambda x: x.get('ifname', '')):
+ if interface.get('linkinfo', {}).get('info_kind') == 'vrf':
+ continue
+
+ # Get the device model; ex. Intel Corporation Ethernet Controller I225-V
+ dev_model = interface.get('parentdev', '')
+ if 'parentdev' in interface:
+ parentdev = interface['parentdev']
+ if re.match(r'^[0-9a-fA-F]{4}:', parentdev):
+ dev_model = cmd(f'lspci -nn -s {parentdev}').split(']:')[1].strip()
+
+ # Get the IP addresses on interface
+ ip_list = []
+ has_global = False
+
+ for ip in interface['addr_info']:
+ if ip.get('scope') in ('global', 'host'):
+ has_global = True
+ local = ip.get('local', '-')
+ prefixlen = ip.get('prefixlen', '')
+ ip_list.append(f"{local}/{prefixlen}")
+
+
+ # If no global IP address, add '-'; indicates no IP address on interface
+ if not has_global:
+ ip_list.append('-')
+
+ sl_status = ('A' if not 'UP' in interface['flags'] else 'u') + '/' + ('D' if interface['operstate'] == 'DOWN' else 'u')
+
+ # Generate temporary dict to hold data
+ tmpInfo['ifname'] = interface.get('ifname', '')
+ tmpInfo['ip'] = ip_list
+ tmpInfo['mac'] = interface.get('address', '')
+ tmpInfo['mtu'] = interface.get('mtu', '')
+ tmpInfo['vrf'] = interface.get('master', 'default')
+ tmpInfo['status'] = sl_status
+ tmpInfo['description'] = interface.get('ifalias', '')
+ tmpInfo['device'] = dev_model
+ tmpInfo['alternate_names'] = interface.get('altnames', '')
+ tmpInfo['minimum_mtu'] = interface.get('min_mtu', '')
+ tmpInfo['maximum_mtu'] = interface.get('max_mtu', '')
+ rx_stats = interface.get('stats64', {}).get('rx')
+ tx_stats = interface.get('stats64', {}).get('tx')
+ tmpInfo['rx_packets'] = rx_stats.get('packets', "")
+ tmpInfo['rx_bytes'] = rx_stats.get('bytes', "")
+ tmpInfo['rx_errors'] = rx_stats.get('errors', "")
+ tmpInfo['rx_dropped'] = rx_stats.get('dropped', "")
+ tmpInfo['rx_over_errors'] = rx_stats.get('over_errors', '')
+ tmpInfo['multicast'] = rx_stats.get('multicast', "")
+ tmpInfo['tx_packets'] = tx_stats.get('packets', "")
+ tmpInfo['tx_bytes'] = tx_stats.get('bytes', "")
+ tmpInfo['tx_errors'] = tx_stats.get('errors', "")
+ tmpInfo['tx_dropped'] = tx_stats.get('dropped', "")
+ tmpInfo['tx_carrier_errors'] = tx_stats.get('carrier_errors', "")
+ tmpInfo['tx_collisions'] = tx_stats.get('collisions', "")
+
+ # Generate output list; detail adds more fields
+ output_list.append([tmpInfo['ifname'],
+ '\n'.join(tmpInfo['ip']),
+ tmpInfo['mac'],
+ tmpInfo['vrf'],
+ tmpInfo['mtu'],
+ tmpInfo['status'],
+ tmpInfo['description'],
+ *([tmpInfo['device']] if detail else []),
+ *(['\n'.join(tmpInfo['alternate_names'])] if detail else []),
+ *([tmpInfo['minimum_mtu']] if detail else []),
+ *([tmpInfo['maximum_mtu']] if detail else []),
+ *([tmpInfo['rx_packets']] if detail else []),
+ *([tmpInfo['rx_bytes']] if detail else []),
+ *([tmpInfo['rx_errors']] if detail else []),
+ *([tmpInfo['rx_dropped']] if detail else []),
+ *([tmpInfo['rx_over_errors']] if detail else []),
+ *([tmpInfo['multicast']] if detail else []),
+ *([tmpInfo['tx_packets']] if detail else []),
+ *([tmpInfo['tx_bytes']] if detail else []),
+ *([tmpInfo['tx_errors']] if detail else []),
+ *([tmpInfo['tx_dropped']] if detail else []),
+ *([tmpInfo['tx_carrier_errors']] if detail else []),
+ *([tmpInfo['tx_collisions']] if detail else [])])
+
+ return output_list
+
@catch_broken_pipe
def _format_show_data(data: list):
unhandled = []
@@ -445,6 +562,27 @@ def _format_show_counters(data: list):
print (output)
return output
+def show_kernel(raw: bool, intf_name: typing.Optional[str], detail: bool):
+ raw_data, data = _get_kernel_data(raw, intf_name, detail)
+
+ # Return early if raw
+ if raw:
+ return raw_data
+
+ # Normal headers; show interfaces kernel
+ headers = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description']
+
+ # Detail headers; show interfaces kernel detail
+ detail_header = ['Interface', 'IP Address', 'MAC', 'VRF', 'MTU', 'S/L', 'Description',
+ 'Device', 'Alternate Names','Minimum MTU', 'Maximum MTU', 'RX_Packets',
+ 'RX_Bytes', 'RX_Errors', 'RX_Dropped', 'Receive Overrun Errors', 'Received Multicast',
+ 'TX_Packets', 'TX_Bytes', 'TX_Errors', 'TX_Dropped', 'Transmit Carrier Errors',
+ 'Transmit Collisions']
+
+ if detail:
+ detailed_output(data, detail_header)
+ else:
+ print(tabulate(data, headers))
def _show_raw(data: list, intf_name: str):
if intf_name is not None and len(data) <= 1:
diff --git a/src/op_mode/load-balancing_wan.py b/src/op_mode/load-balancing_wan.py
new file mode 100755
index 000000000..9fa473802
--- /dev/null
+++ b/src/op_mode/load-balancing_wan.py
@@ -0,0 +1,117 @@
+#!/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 re
+import sys
+
+from datetime import datetime
+
+from vyos.config import Config
+from vyos.utils.process import cmd
+
+import vyos.opmode
+
+wlb_status_file = '/run/wlb_status.json'
+
+status_format = '''Interface: {ifname}
+Status: {status}
+Last Status Change: {last_change}
+Last Interface Success: {last_success}
+Last Interface Failure: {last_failure}
+Interface Failures: {failures}
+'''
+
+def _verify(func):
+ """Decorator checks if WLB config exists"""
+ from functools import wraps
+
+ @wraps(func)
+ def _wrapper(*args, **kwargs):
+ config = Config()
+ if not config.exists(['load-balancing', 'wan']):
+ unconf_message = 'WAN load-balancing is not configured'
+ raise vyos.opmode.UnconfiguredSubsystem(unconf_message)
+ return func(*args, **kwargs)
+ return _wrapper
+
+def _get_raw_data():
+ with open(wlb_status_file, 'r') as f:
+ data = json.loads(f.read())
+ if not data:
+ return {}
+ return data
+
+def _get_formatted_output(raw_data):
+ for ifname, if_data in raw_data.items():
+ latest_change = if_data['last_success'] if if_data['last_success'] > if_data['last_failure'] else if_data['last_failure']
+
+ change_dt = datetime.fromtimestamp(latest_change) if latest_change > 0 else None
+ success_dt = datetime.fromtimestamp(if_data['last_success']) if if_data['last_success'] > 0 else None
+ failure_dt = datetime.fromtimestamp(if_data['last_failure']) if if_data['last_failure'] > 0 else None
+ now = datetime.utcnow()
+
+ fmt_data = {
+ 'ifname': ifname,
+ 'status': "active" if if_data['state'] else "failed",
+ 'last_change': change_dt.strftime("%Y-%m-%d %H:%M:%S") if change_dt else 'N/A',
+ 'last_success': str(now - success_dt) if success_dt else 'N/A',
+ 'last_failure': str(now - failure_dt) if failure_dt else 'N/A',
+ 'failures': if_data['failure_count']
+ }
+ print(status_format.format(**fmt_data))
+
+@_verify
+def show_summary(raw: bool):
+ data = _get_raw_data()
+
+ if raw:
+ return data
+ else:
+ return _get_formatted_output(data)
+
+@_verify
+def show_connection(raw: bool):
+ res = cmd('sudo conntrack -L -n')
+ lines = res.split("\n")
+ filtered_lines = [line for line in lines if re.search(r' mark=[1-9]', line)]
+
+ if raw:
+ return filtered_lines
+
+ for line in lines:
+ print(line)
+
+@_verify
+def show_status(raw: bool):
+ res = cmd('sudo nft list chain ip vyos_wanloadbalance wlb_mangle_prerouting')
+ lines = res.split("\n")
+ filtered_lines = [line.replace("\t", "") for line in lines[3:-2] if 'meta mark set' not in line]
+
+ if raw:
+ return filtered_lines
+
+ for line in filtered_lines:
+ print(line)
+
+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/qos.py b/src/op_mode/qos.py
index b8ca149a0..464b552ee 100755
--- a/src/op_mode/qos.py
+++ b/src/op_mode/qos.py
@@ -38,7 +38,7 @@ def get_tc_info(interface_dict, interface_name, policy_type):
if not policy_name:
return None, None
- class_dict = op_mode_config_dict(['qos', 'policy', policy_type, policy_name], key_mangling=('-', '_'),
+ class_dict = op_mode_config_dict(['qos', 'policy', policy_type, policy_name],
get_first_key=True)
if not class_dict:
return None, None
diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py
index 3b0031f34..efa835485 100755
--- a/src/op_mode/restart.py
+++ b/src/op_mode/restart.py
@@ -53,6 +53,10 @@ service_map = {
'systemd_service': 'strongswan',
'path': ['vpn', 'ipsec'],
},
+ 'load-balancing_wan': {
+ 'systemd_service': 'vyos-wan-load-balance',
+ 'path': ['load-balancing', 'wan'],
+ },
'mdns_repeater': {
'systemd_service': 'avahi-daemon',
'path': ['service', 'mdns', 'repeater'],
@@ -86,6 +90,7 @@ services = typing.Literal[
'haproxy',
'igmp_proxy',
'ipsec',
+ 'load-balancing_wan',
'mdns_repeater',
'router_advert',
'snmp',
diff --git a/src/op_mode/stp.py b/src/op_mode/stp.py
new file mode 100755
index 000000000..fb57bd7ee
--- /dev/null
+++ b/src/op_mode/stp.py
@@ -0,0 +1,185 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 typing
+import json
+from tabulate import tabulate
+
+import vyos.opmode
+from vyos.utils.process import cmd
+from vyos.utils.network import interface_exists
+
+def detailed_output(dataset, headers):
+ for data in dataset:
+ adjusted_rule = data + [""] * (len(headers) - len(data)) # account for different header length, like default-action
+ transformed_rule = [[header, adjusted_rule[i]] for i, header in enumerate(headers) if i < len(adjusted_rule)] # create key-pair list from headers and rules lists; wrap at 100 char
+
+ print(tabulate(transformed_rule, tablefmt="presto"))
+ print()
+
+def _get_bridge_vlan_data(iface):
+ allowed_vlans = []
+ native_vlan = None
+ vlanData = json.loads(cmd(f"bridge -j -d vlan show"))
+ for vlans in vlanData:
+ if vlans['ifname'] == iface:
+ for allowed in vlans['vlans']:
+ if "flags" in allowed and "PVID" in allowed["flags"]:
+ native_vlan = allowed['vlan']
+ elif allowed.get('vlanEnd', None):
+ allowed_vlans.append(f"{allowed['vlan']}-{allowed['vlanEnd']}")
+ else:
+ allowed_vlans.append(str(allowed['vlan']))
+
+ if not allowed_vlans:
+ allowed_vlans = ["none"]
+ if not native_vlan:
+ native_vlan = "none"
+
+ return ",".join(allowed_vlans), native_vlan
+
+def _get_stp_data(ifname, brInfo, brStatus):
+ tmpInfo = {}
+
+ tmpInfo['bridge_name'] = brInfo.get('ifname')
+ tmpInfo['up_state'] = brInfo.get('operstate')
+ tmpInfo['priority'] = brInfo.get('linkinfo').get('info_data').get('priority')
+ tmpInfo['vlan_filtering'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('vlan_filtering') == 1 else "Disabled"
+ tmpInfo['vlan_protocol'] = brInfo.get('linkinfo').get('info_data').get('vlan_protocol')
+
+ # The version of VyOS I tested had am issue with the "ip -d link show type bridge"
+ # output. The root_id was always the local bridge, even though the underlying system
+ # understood when it wasn't. Could be an upstream Bug. I pull from the "/sys/class/net"
+ # structure instead. This can be changed later if the "ip link" behavior is corrected.
+
+ #tmpInfo['bridge_id'] = brInfo.get('linkinfo').get('info_data').get('bridge_id')
+ #tmpInfo['root_id'] = brInfo.get('linkinfo').get('info_data').get('root_id')
+
+ tmpInfo['bridge_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/bridge_id").split('.')
+ tmpInfo['root_id'] = cmd(f"cat /sys/class/net/{brInfo.get('ifname')}/bridge/root_id").split('.')
+
+ # The "/sys/class/net" structure stores the IDs without seperators like ':' or '.'
+ # This adds a ':' after every 2 characters to make it resemble a MAC Address
+ tmpInfo['bridge_id'][1] = ':'.join(tmpInfo['bridge_id'][1][i:i+2] for i in range(0, len(tmpInfo['bridge_id'][1]), 2))
+ tmpInfo['root_id'][1] = ':'.join(tmpInfo['root_id'][1][i:i+2] for i in range(0, len(tmpInfo['root_id'][1]), 2))
+
+ tmpInfo['stp_state'] = "Enabled" if brInfo.get('linkinfo', {}).get('info_data', {}).get('stp_state') == 1 else "Disabled"
+
+ # I don't call any of these values, but I created them to be called within raw output if desired
+
+ tmpInfo['mcast_snooping'] = "Enabled" if brInfo.get('linkinfo').get('info_data').get('mcast_snooping') == 1 else "Disabled"
+ tmpInfo['rxbytes'] = brInfo.get('stats64').get('rx').get('bytes')
+ tmpInfo['rxpackets'] = brInfo.get('stats64').get('rx').get('packets')
+ tmpInfo['rxerrors'] = brInfo.get('stats64').get('rx').get('errors')
+ tmpInfo['rxdropped'] = brInfo.get('stats64').get('rx').get('dropped')
+ tmpInfo['rxover_errors'] = brInfo.get('stats64').get('rx').get('over_errors')
+ tmpInfo['rxmulticast'] = brInfo.get('stats64').get('rx').get('multicast')
+ tmpInfo['txbytes'] = brInfo.get('stats64').get('tx').get('bytes')
+ tmpInfo['txpackets'] = brInfo.get('stats64').get('tx').get('packets')
+ tmpInfo['txerrors'] = brInfo.get('stats64').get('tx').get('errors')
+ tmpInfo['txdropped'] = brInfo.get('stats64').get('tx').get('dropped')
+ tmpInfo['txcarrier_errors'] = brInfo.get('stats64').get('tx').get('carrier_errors')
+ tmpInfo['txcollosions'] = brInfo.get('stats64').get('tx').get('collisions')
+
+ tmpStatus = []
+ for members in brStatus:
+ if members.get('master') == brInfo.get('ifname'):
+ allowed_vlans, native_vlan = _get_bridge_vlan_data(members['ifname'])
+ tmpStatus.append({'interface': members.get('ifname'),
+ 'state': members.get('state').capitalize(),
+ 'mtu': members.get('mtu'),
+ 'pathcost': members.get('cost'),
+ 'bpduguard': "Enabled" if members.get('guard') == True else "Disabled",
+ 'rootguard': "Enabled" if members.get('root_block') == True else "Disabled",
+ 'mac_learning': "Enabled" if members.get('learning') == True else "Disabled",
+ 'neigh_suppress': "Enabled" if members.get('neigh_suppress') == True else "Disabled",
+ 'vlan_tunnel': "Enabled" if members.get('vlan_tunnel') == True else "Disabled",
+ 'isolated': "Enabled" if members.get('isolated') == True else "Disabled",
+ **({'allowed_vlans': allowed_vlans} if allowed_vlans else {}),
+ **({'native_vlan': native_vlan} if native_vlan else {})})
+
+ tmpInfo['members'] = tmpStatus
+ return tmpInfo
+
+def show_stp(raw: bool, ifname: typing.Optional[str], detail: bool):
+ rawList = []
+ rawDict = {'stp': []}
+
+ if ifname:
+ if not interface_exists(ifname):
+ raise vyos.opmode.Error(f"{ifname} does not exist!")
+ else:
+ ifname = ""
+
+ bridgeInfo = json.loads(cmd(f"ip -j -d -s link show type bridge {ifname}"))
+
+ if not bridgeInfo:
+ raise vyos.opmode.Error(f"No Bridges configured!")
+
+ bridgeStatus = json.loads(cmd(f"bridge -j -s -d link show"))
+
+ for bridges in bridgeInfo:
+ output_list = []
+ amRoot = ""
+ bridgeDict = _get_stp_data(ifname, bridges, bridgeStatus)
+
+ if bridgeDict['bridge_id'][1] == bridgeDict['root_id'][1]:
+ amRoot = " (This bridge is the root)"
+
+ print('-' * 80)
+ print(f"Bridge interface {bridgeDict['bridge_name']} ({bridgeDict['up_state']}):\n")
+ print(f"Spanning Tree is {bridgeDict['stp_state']}")
+ print(f"Bridge ID {bridgeDict['bridge_id'][1]}, Priority {int(bridgeDict['bridge_id'][0], 16)}")
+ print(f"Root ID {bridgeDict['root_id'][1]}, Priority {int(bridgeDict['root_id'][0], 16)}{amRoot}")
+ print(f"VLANs {bridgeDict['vlan_filtering'].capitalize()}, Protocol {bridgeDict['vlan_protocol']}")
+ print()
+
+ for members in bridgeDict['members']:
+ output_list.append([members['interface'],
+ members['state'],
+ *([members['pathcost']] if detail else []),
+ members['bpduguard'],
+ members['rootguard'],
+ members['mac_learning'],
+ *([members['neigh_suppress']] if detail else []),
+ *([members['vlan_tunnel']] if detail else []),
+ *([members['isolated']] if detail else []),
+ *([members['allowed_vlans']] if detail else []),
+ *([members['native_vlan']] if detail else [])])
+
+ if raw:
+ rawList.append(bridgeDict)
+ elif detail:
+ headers = ['Interface', 'State', 'Pathcost', 'BPDU_Guard', 'Root_Guard', 'Learning', 'Neighbor_Suppression', 'Q-in-Q', 'Port_Isolation', 'Allowed VLANs', 'Native VLAN']
+ detailed_output(output_list, headers)
+ else:
+ headers = ['Interface', 'State', 'BPDU_Guard', 'Root_Guard', 'Learning']
+ print(tabulate(output_list, headers))
+ print()
+
+ if raw:
+ rawDict['stp'] = rawList
+ return rawDict
+
+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/tech_support.py b/src/op_mode/tech_support.py
index 24ac0af1b..c4496dfa3 100644
--- a/src/op_mode/tech_support.py
+++ b/src/op_mode/tech_support.py
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2024 VyOS maintainers and contributors
+# Copyright (C) 2025 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,6 +20,7 @@ import json
import vyos.opmode
from vyos.utils.process import cmd
+from vyos.base import Warning
def _get_version_data():
from vyos.version import get_version_data
@@ -51,7 +52,12 @@ def _get_storage():
def _get_devices():
devices = {}
devices["pci"] = cmd("lspci")
- devices["usb"] = cmd("lsusb")
+
+ try:
+ devices["usb"] = cmd("lsusb")
+ except OSError:
+ Warning("Could not retrieve information about USB devices")
+ devices["usb"] = {}
return devices
diff --git a/src/services/api/rest/models.py b/src/services/api/rest/models.py
index 27d9fb5ee..dda50010f 100644
--- a/src/services/api/rest/models.py
+++ b/src/services/api/rest/models.py
@@ -293,6 +293,13 @@ class TracerouteModel(ApiModel):
}
+class InfoQueryParams(BaseModel):
+ model_config = {"extra": "forbid"}
+
+ version: bool = True
+ hostname: bool = True
+
+
class Success(BaseModel):
success: bool
data: Union[str, bool, Dict]
diff --git a/src/services/vyos-commitd b/src/services/vyos-commitd
new file mode 100755
index 000000000..e7f2d82c7
--- /dev/null
+++ b/src/services/vyos-commitd
@@ -0,0 +1,457 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 sys
+import grp
+import json
+import signal
+import socket
+import typing
+import logging
+import traceback
+import importlib.util
+import io
+from contextlib import redirect_stdout
+from dataclasses import dataclass
+from dataclasses import fields
+from dataclasses import field
+from dataclasses import asdict
+from pathlib import Path
+
+import tomli
+
+from google.protobuf.json_format import MessageToDict
+from google.protobuf.json_format import ParseDict
+
+from vyos.defaults import directories
+from vyos.utils.boot import boot_configuration_complete
+from vyos.configsource import ConfigSourceCache
+from vyos.configsource import ConfigSourceError
+from vyos.config import Config
+from vyos.frrender import FRRender
+from vyos.frrender import get_frrender_dict
+from vyos import ConfigError
+
+from vyos.proto import vycall_pb2
+
+
+@dataclass
+class Status:
+ success: bool = False
+ out: str = ''
+
+
+@dataclass
+class Call:
+ script_name: str = ''
+ tag_value: str = None
+ arg_value: str = None
+ reply: Status = None
+
+ def set_reply(self, success: bool, out: str):
+ self.reply = Status(success=success, out=out)
+
+
+@dataclass
+class Session:
+ # pylint: disable=too-many-instance-attributes
+
+ session_id: str = ''
+ dry_run: bool = False
+ atomic: bool = False
+ background: bool = False
+ config: Config = None
+ init: Status = None
+ calls: list[Call] = field(default_factory=list)
+
+ def set_init(self, success: bool, out: str):
+ self.init = Status(success=success, out=out)
+
+
+@dataclass
+class ServerConf:
+ commitd_socket: str = ''
+ session_dir: str = ''
+ running_cache: str = ''
+ session_cache: str = ''
+
+
+server_conf = None
+SOCKET_PATH = None
+conf_mode_scripts = None
+frr = None
+
+CFG_GROUP = 'vyattacfg'
+
+script_stdout_log = '/tmp/vyos-commitd-script-stdout'
+
+debug = True
+
+logger = logging.getLogger(__name__)
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+if debug:
+ logger.setLevel(logging.DEBUG)
+else:
+ logger.setLevel(logging.INFO)
+
+
+vyos_conf_scripts_dir = directories['conf_mode']
+commitd_include_file = os.path.join(directories['data'], 'configd-include.json')
+
+
+def key_name_from_file_name(f):
+ return os.path.splitext(f)[0]
+
+
+def module_name_from_key(k):
+ return k.replace('-', '_')
+
+
+def path_from_file_name(f):
+ return os.path.join(vyos_conf_scripts_dir, f)
+
+
+def load_conf_mode_scripts():
+ with open(commitd_include_file) as f:
+ try:
+ include = json.load(f)
+ except OSError as e:
+ logger.critical(f'configd include file error: {e}')
+ sys.exit(1)
+ except json.JSONDecodeError as e:
+ logger.critical(f'JSON load error: {e}')
+ sys.exit(1)
+
+ # import conf_mode scripts
+ (_, _, filenames) = next(iter(os.walk(vyos_conf_scripts_dir)))
+ filenames.sort()
+
+ # this is redundant, as all scripts are currently in the include file;
+ # leave it as an inexpensive check for future changes
+ load_filenames = [f for f in filenames if f in include]
+ imports = [key_name_from_file_name(f) for f in load_filenames]
+ module_names = [module_name_from_key(k) for k in imports]
+ paths = [path_from_file_name(f) for f in load_filenames]
+ to_load = list(zip(module_names, paths))
+
+ modules = []
+
+ for x in to_load:
+ spec = importlib.util.spec_from_file_location(x[0], x[1])
+ module = importlib.util.module_from_spec(spec)
+ spec.loader.exec_module(module)
+ modules.append(module)
+
+ scripts = dict(zip(imports, modules))
+
+ return scripts
+
+
+def get_session_out(session: Session) -> str:
+ out = ''
+ if session.init and session.init.out:
+ out = f'{out} + init: {session.init.out} + \n'
+ for call in session.calls:
+ reply = call.reply
+ if reply and reply.out:
+ out = f'{out} + {call.script_name}: {reply.out} + \n'
+ return out
+
+
+def write_stdout_log(file_name, session):
+ if boot_configuration_complete():
+ return
+ with open(file_name, 'a') as f:
+ f.write(get_session_out(session))
+
+
+def msg_to_commit_data(msg: vycall_pb2.Commit) -> Session:
+ # pylint: disable=no-member
+
+ d = MessageToDict(msg, preserving_proto_field_name=True)
+
+ # wrap in dataclasses
+ session = Session(**d)
+ session.init = Status(**session.init) if session.init else None
+ session.calls = list(map(lambda x: Call(**x), session.calls))
+ for call in session.calls:
+ call.reply = Status(**call.reply) if call.reply else None
+
+ return session
+
+
+def commit_data_to_msg(obj: Session) -> vycall_pb2.Commit:
+ # pylint: disable=no-member
+
+ # avoid asdict attempt of deepcopy on Config obj
+ obj.config = None
+
+ msg = vycall_pb2.Commit()
+ msg = ParseDict(asdict(obj), msg, ignore_unknown_fields=True)
+
+ return msg
+
+
+def initialization(session: Session) -> Session:
+ running_cache = os.path.join(server_conf.session_dir, server_conf.running_cache)
+ session_cache = os.path.join(server_conf.session_dir, server_conf.session_cache)
+ try:
+ configsource = ConfigSourceCache(
+ running_config_cache=running_cache,
+ session_config_cache=session_cache,
+ )
+ except ConfigSourceError as e:
+ fail_msg = f'Failed to read config caches: {e}'
+ logger.critical(fail_msg)
+ session.set_init(False, fail_msg)
+ return session
+
+ session.set_init(True, '')
+
+ config = Config(config_source=configsource)
+
+ dependent_func: dict[str, list[typing.Callable]] = {}
+ setattr(config, 'dependent_func', dependent_func)
+
+ scripts_called = []
+ setattr(config, 'scripts_called', scripts_called)
+
+ dry_run = session.dry_run
+ config.set_bool_attr('dry_run', dry_run)
+ logger.debug(f'commit dry_run is {dry_run}')
+
+ session.config = config
+
+ return session
+
+
+def run_script(script_name: str, config: Config, args: list) -> tuple[bool, str]:
+ # pylint: disable=broad-exception-caught
+
+ script = conf_mode_scripts[script_name]
+ script.argv = args
+ config.set_level([])
+ dry_run = config.get_bool_attr('dry_run')
+ try:
+ c = script.get_config(config)
+ script.verify(c)
+ if not dry_run:
+ script.generate(c)
+ script.apply(c)
+ else:
+ if hasattr(script, 'call_dependents'):
+ script.call_dependents()
+ except ConfigError as e:
+ logger.error(e)
+ return False, str(e)
+ except Exception:
+ tb = traceback.format_exc()
+ logger.error(tb)
+ return False, tb
+
+ return True, ''
+
+
+def process_call_data(call: Call, config: Config, last: bool = False) -> None:
+ # pylint: disable=too-many-locals
+
+ script_name = key_name_from_file_name(call.script_name)
+
+ if script_name not in conf_mode_scripts:
+ fail_msg = f'No such script: {call.script_name}'
+ logger.critical(fail_msg)
+ call.set_reply(False, fail_msg)
+ return
+
+ config.dependency_list.clear()
+
+ tag_value = call.tag_value if call.tag_value is not None else ''
+ os.environ['VYOS_TAGNODE_VALUE'] = tag_value
+
+ args = call.arg_value.split() if call.arg_value else []
+ args.insert(0, f'{script_name}.py')
+
+ tag_ext = f'_{tag_value}' if tag_value else ''
+ script_record = f'{script_name}{tag_ext}'
+ scripts_called = getattr(config, 'scripts_called', [])
+ scripts_called.append(script_record)
+
+ with redirect_stdout(io.StringIO()) as o:
+ success, err_out = run_script(script_name, config, args)
+ amb_out = o.getvalue()
+ o.close()
+
+ out = amb_out + err_out
+
+ call.set_reply(success, out)
+
+ logger.info(f'[{script_name}] {out}')
+
+ if last:
+ scripts_called = getattr(config, 'scripts_called', [])
+ logger.debug(f'scripts_called: {scripts_called}')
+
+ if last and success:
+ tmp = get_frrender_dict(config)
+ if frr.generate(tmp):
+ # only apply a new FRR configuration if anything changed
+ # in comparison to the previous applied configuration
+ frr.apply()
+
+
+def process_session_data(session: Session) -> Session:
+ if session.init is None or not session.init.success:
+ return session
+
+ config = session.config
+ len_calls = len(session.calls)
+ for index, call in enumerate(session.calls):
+ process_call_data(call, config, last=len_calls == index + 1)
+
+ return session
+
+
+def read_message(msg: bytes) -> Session:
+ """Read message into Session instance"""
+
+ message = vycall_pb2.Commit() # pylint: disable=no-member
+ message.ParseFromString(msg)
+ session = msg_to_commit_data(message)
+
+ session = initialization(session)
+ session = process_session_data(session)
+
+ write_stdout_log(script_stdout_log, session)
+
+ return session
+
+
+def write_reply(session: Session) -> bytearray:
+ """Serialize modified object to bytearray, prepending data length
+ header"""
+
+ reply = commit_data_to_msg(session)
+ encoded_data = reply.SerializeToString()
+ byte_size = reply.ByteSize()
+ length_bytes = byte_size.to_bytes(4)
+ arr = bytearray(length_bytes)
+ arr.extend(encoded_data)
+
+ return arr
+
+
+def load_server_conf() -> ServerConf:
+ # pylint: disable=import-outside-toplevel
+ # pylint: disable=broad-exception-caught
+ from vyos.defaults import vyconfd_conf
+
+ try:
+ with open(vyconfd_conf, 'rb') as f:
+ vyconfd_conf_d = tomli.load(f)
+
+ except Exception as e:
+ logger.critical(f'Failed to open the vyconfd.conf file {vyconfd_conf}: {e}')
+ sys.exit(1)
+
+ app = vyconfd_conf_d.get('appliance', {})
+
+ conf_data = {
+ k: v for k, v in app.items() if k in [_.name for _ in fields(ServerConf)]
+ }
+
+ conf = ServerConf(**conf_data)
+
+ return conf
+
+
+def remove_if_exists(f: str):
+ try:
+ os.unlink(f)
+ except FileNotFoundError:
+ pass
+
+
+def sig_handler(_signum, _frame):
+ logger.info('stopping server')
+ raise KeyboardInterrupt
+
+
+def run_server():
+ # pylint: disable=global-statement
+
+ global server_conf
+ global SOCKET_PATH
+ global conf_mode_scripts
+ global frr
+
+ signal.signal(signal.SIGTERM, sig_handler)
+ signal.signal(signal.SIGINT, sig_handler)
+
+ logger.info('starting server')
+
+ server_conf = load_server_conf()
+ SOCKET_PATH = server_conf.commitd_socket
+ conf_mode_scripts = load_conf_mode_scripts()
+
+ cfg_group = grp.getgrnam(CFG_GROUP)
+ os.setgid(cfg_group.gr_gid)
+
+ server_socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
+
+ remove_if_exists(SOCKET_PATH)
+ server_socket.bind(SOCKET_PATH)
+ Path(SOCKET_PATH).chmod(0o775)
+
+ # We only need one long-lived instance of FRRender
+ frr = FRRender()
+
+ server_socket.listen(2)
+ while True:
+ try:
+ conn, _ = server_socket.accept()
+ logger.debug('connection accepted')
+ while True:
+ # receive size of data
+ data_length = conn.recv(4)
+ if not data_length:
+ logger.debug('no data')
+ # if no data break
+ break
+
+ length = int.from_bytes(data_length)
+ # receive data
+ data = conn.recv(length)
+
+ session = read_message(data)
+ reply = write_reply(session)
+ conn.sendall(reply)
+
+ conn.close()
+ logger.debug('connection closed')
+
+ except KeyboardInterrupt:
+ break
+
+ server_socket.close()
+ sys.exit(0)
+
+
+if __name__ == '__main__':
+ run_server()
diff --git a/src/services/vyos-configd b/src/services/vyos-configd
index b161fe6ba..28acccd2c 100755
--- a/src/services/vyos-configd
+++ b/src/services/vyos-configd
@@ -28,6 +28,7 @@ import traceback
import importlib.util
import io
from contextlib import redirect_stdout
+from enum import Enum
import zmq
@@ -60,11 +61,14 @@ SOCKET_PATH = 'ipc:///run/vyos-configd.sock'
MAX_MSG_SIZE = 65535
PAD_MSG_SIZE = 6
+
# Response error codes
-R_SUCCESS = 1
-R_ERROR_COMMIT = 2
-R_ERROR_DAEMON = 4
-R_PASS = 8
+class Response(Enum):
+ SUCCESS = 1
+ ERROR_COMMIT = 2
+ ERROR_DAEMON = 4
+ PASS = 8
+
vyos_conf_scripts_dir = directories['conf_mode']
configd_include_file = os.path.join(directories['data'], 'configd-include.json')
@@ -73,12 +77,15 @@ configd_env_unset_file = os.path.join(directories['data'], 'vyos-configd-env-uns
# sourced on entering config session
configd_env_file = '/etc/default/vyos-configd-env'
+
def key_name_from_file_name(f):
return os.path.splitext(f)[0]
+
def module_name_from_key(k):
return k.replace('-', '_')
+
def path_from_file_name(f):
return os.path.join(vyos_conf_scripts_dir, f)
@@ -126,7 +133,7 @@ def write_stdout_log(file_name, msg):
f.write(msg)
-def run_script(script_name, config, args) -> tuple[int, str]:
+def run_script(script_name, config, args) -> tuple[Response, str]:
# pylint: disable=broad-exception-caught
script = conf_mode_scripts[script_name]
@@ -139,13 +146,13 @@ def run_script(script_name, config, args) -> tuple[int, str]:
script.apply(c)
except ConfigError as e:
logger.error(e)
- return R_ERROR_COMMIT, str(e)
+ return Response.ERROR_COMMIT, str(e)
except Exception:
tb = traceback.format_exc()
logger.error(tb)
- return R_ERROR_COMMIT, tb
+ return Response.ERROR_COMMIT, tb
- return R_SUCCESS, ''
+ return Response.SUCCESS, ''
def initialization(socket):
@@ -195,8 +202,9 @@ def initialization(socket):
os.environ['VYATTA_CHANGES_ONLY_DIR'] = changes_only_dir_string
try:
- configsource = ConfigSourceString(running_config_text=active_string,
- session_config_text=session_string)
+ configsource = ConfigSourceString(
+ running_config_text=active_string, session_config_text=session_string
+ )
except ConfigSourceError as e:
logger.debug(e)
return None
@@ -214,11 +222,11 @@ def initialization(socket):
return config
-def process_node_data(config, data, _last: bool = False) -> tuple[int, str]:
+def process_node_data(config, data, _last: bool = False) -> tuple[Response, str]:
if not config:
out = 'Empty config'
logger.critical(out)
- return R_ERROR_DAEMON, out
+ return Response.ERROR_DAEMON, out
script_name = None
os.environ['VYOS_TAGNODE_VALUE'] = ''
@@ -234,7 +242,7 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]:
if not script_name:
out = 'Missing script_name'
logger.critical(out)
- return R_ERROR_DAEMON, out
+ return Response.ERROR_DAEMON, out
if res.group(3):
args = res.group(3).split()
args.insert(0, f'{script_name}.py')
@@ -246,7 +254,7 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]:
scripts_called.append(script_record)
if script_name not in include_set:
- return R_PASS, ''
+ return Response.PASS, ''
with redirect_stdout(io.StringIO()) as o:
result, err_out = run_script(script_name, config, args)
@@ -259,13 +267,15 @@ def process_node_data(config, data, _last: bool = False) -> tuple[int, str]:
def send_result(sock, err, msg):
+ err_no = err.value
+ err_name = err.name
msg = msg if msg else ''
msg_size = min(MAX_MSG_SIZE, len(msg))
- err_rep = err.to_bytes(1)
+ err_rep = err_no.to_bytes(1)
msg_size_rep = f'{msg_size:#0{PAD_MSG_SIZE}x}'
- logger.debug(f'Sending reply: error_code {err} with output')
+ logger.debug(f'Sending reply: {err_name} with output')
sock.send_multipart([err_rep, msg_size_rep.encode(), msg.encode()])
write_stdout_log(script_stdout_log, msg)
@@ -331,7 +341,7 @@ if __name__ == '__main__':
scripts_called = getattr(config, 'scripts_called', [])
logger.debug(f'scripts_called: {scripts_called}')
- if res == R_SUCCESS:
+ if res == Response.SUCCESS:
tmp = get_frrender_dict(config)
if frr.generate(tmp):
# only apply a new FRR configuration if anything changed
diff --git a/src/services/vyos-conntrack-logger b/src/services/vyos-conntrack-logger
index 9c31b465f..ec0e1f717 100755
--- a/src/services/vyos-conntrack-logger
+++ b/src/services/vyos-conntrack-logger
@@ -15,10 +15,8 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import argparse
-import grp
import logging
import multiprocessing
-import os
import queue
import signal
import socket
diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver
index fe0f40a07..fb18724af 100755
--- a/src/services/vyos-domain-resolver
+++ b/src/services/vyos-domain-resolver
@@ -13,19 +13,22 @@
#
# 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 time
import logging
+import os
from vyos.configdict import dict_merge
from vyos.configquery import ConfigTreeQuery
from vyos.firewall import fqdn_config_parse
from vyos.firewall import fqdn_resolve
from vyos.ifconfig import WireGuardIf
+from vyos.remote import download
from vyos.utils.commit import commit_in_progress
from vyos.utils.dict import dict_search_args
from vyos.utils.kernel import WIREGUARD_REKEY_AFTER_TIME
+from vyos.utils.file import makedir, chmod_775, write_file, read_file
+from vyos.utils.network import is_valid_ipv4_address_or_range, is_valid_ipv6_address_or_range
from vyos.utils.process import cmd
from vyos.utils.process import run
from vyos.xml_ref import get_defaults
@@ -37,6 +40,8 @@ base_firewall = ['firewall']
base_nat = ['nat']
base_interfaces = ['interfaces']
+firewall_config_dir = "/config/firewall"
+
domain_state = {}
ipv4_tables = {
@@ -65,13 +70,15 @@ def get_config(conf, node):
node_config = dict_merge(default_values, node_config)
- global timeout, cache
+ if node == base_firewall and 'global_options' in node_config:
+ global_config = node_config['global_options']
+ global timeout, cache
- if 'resolver_interval' in node_config:
- timeout = int(node_config['resolver_interval'])
+ if 'resolver_interval' in global_config:
+ timeout = int(global_config['resolver_interval'])
- if 'resolver_cache' in node_config:
- cache = True
+ if 'resolver_cache' in global_config:
+ cache = True
fqdn_config_parse(node_config, node[0])
@@ -85,12 +92,14 @@ def resolve(domains, ipv6=False):
for domain in domains:
resolved = fqdn_resolve(domain, ipv6=ipv6)
+ cache_key = f'{domain}_ipv6' if ipv6 else domain
+
if resolved and cache:
- domain_state[domain] = resolved
+ domain_state[cache_key] = resolved
elif not resolved:
- if domain not in domain_state:
+ if cache_key not in domain_state:
continue
- resolved = domain_state[domain]
+ resolved = domain_state[cache_key]
ip_list = ip_list | resolved
return ip_list
@@ -119,6 +128,73 @@ def nft_valid_sets():
except:
return []
+def update_remote_group(config):
+ conf_lines = []
+ count = 0
+ valid_sets = nft_valid_sets()
+
+ remote_groups = dict_search_args(config, 'group', 'remote_group')
+ if remote_groups:
+ # Create directory for list files if necessary
+ if not os.path.isdir(firewall_config_dir):
+ makedir(firewall_config_dir, group='vyattacfg')
+ chmod_775(firewall_config_dir)
+
+ for set_name, remote_config in remote_groups.items():
+ if 'url' not in remote_config:
+ continue
+ nft_ip_set_name = f'R_{set_name}'
+ nft_ip6_set_name = f'R6_{set_name}'
+
+ # Create list file if necessary
+ list_file = os.path.join(firewall_config_dir, f"{nft_ip_set_name}.txt")
+ if not os.path.exists(list_file):
+ write_file(list_file, '', user="root", group="vyattacfg", mode=0o644)
+
+ # Attempt to download file, use cached version if download fails
+ try:
+ download(list_file, remote_config['url'], raise_error=True)
+ except:
+ logger.error(f'Failed to download list-file for {set_name} remote group')
+ logger.info(f'Using cached list-file for {set_name} remote group')
+
+ # Read list file
+ ip_list = []
+ ip6_list = []
+ invalid_list = []
+ for line in read_file(list_file).splitlines():
+ line_first_word = line.strip().partition(' ')[0]
+
+ if is_valid_ipv4_address_or_range(line_first_word):
+ ip_list.append(line_first_word)
+ elif is_valid_ipv6_address_or_range(line_first_word):
+ ip6_list.append(line_first_word)
+ else:
+ if line_first_word[0].isalnum():
+ invalid_list.append(line_first_word)
+
+ # Load ip tables
+ for table in ipv4_tables:
+ if (table, nft_ip_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_ip_set_name, ip_list)
+
+ # Load ip6 tables
+ for table in ipv6_tables:
+ if (table, nft_ip6_set_name) in valid_sets:
+ conf_lines += nft_output(table, nft_ip6_set_name, ip6_list)
+
+ invalid_str = ", ".join(invalid_list)
+ if invalid_str:
+ logger.info(f'Invalid address for set {set_name}: {invalid_str}')
+
+ count += 1
+
+ nft_conf_str = "\n".join(conf_lines) + "\n"
+ code = run(f'nft --file -', input=nft_conf_str)
+
+ logger.info(f'Updated {count} remote-groups in firewall - result: {code}')
+
+
def update_fqdn(config, node):
conf_lines = []
count = 0
@@ -177,39 +253,40 @@ def update_fqdn(config, node):
def update_interfaces(config, node):
if node == 'interfaces':
wg_interfaces = dict_search_args(config, 'wireguard')
+ if wg_interfaces:
+
+ peer_public_keys = {}
+ # for each wireguard interfaces
+ for interface, wireguard in wg_interfaces.items():
+ peer_public_keys[interface] = []
+ for peer, peer_config in wireguard['peer'].items():
+ # check peer if peer host-name or address is set
+ if 'host_name' in peer_config or 'address' in peer_config:
+ # check latest handshake
+ peer_public_keys[interface].append(
+ peer_config['public_key']
+ )
+
+ now_time = time.time()
+ for (interface, check_peer_public_keys) in peer_public_keys.items():
+ if len(check_peer_public_keys) == 0:
+ continue
- peer_public_keys = {}
- # for each wireguard interfaces
- for interface, wireguard in wg_interfaces.items():
- peer_public_keys[interface] = []
- for peer, peer_config in wireguard['peer'].items():
- # check peer if peer host-name or address is set
- if 'host_name' in peer_config or 'address' in peer_config:
- # check latest handshake
- peer_public_keys[interface].append(
- peer_config['public_key']
- )
-
- now_time = time.time()
- for (interface, check_peer_public_keys) in peer_public_keys.items():
- if len(check_peer_public_keys) == 0:
- continue
-
- intf = WireGuardIf(interface, create=False, debug=False)
- handshakes = intf.operational.get_latest_handshakes()
-
- # WireGuard performs a handshake every WIREGUARD_REKEY_AFTER_TIME
- # if data is being transmitted between the peers. If no data is
- # transmitted, the handshake will not be initiated unless new
- # data begins to flow. Each handshake generates a new session
- # key, and the key is rotated at least every 120 seconds or
- # upon data transmission after a prolonged silence.
- for public_key, handshake_time in handshakes.items():
- if public_key in check_peer_public_keys and (
- handshake_time == 0
- or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME)
- ):
- intf.operational.reset_peer(public_key=public_key)
+ intf = WireGuardIf(interface, create=False, debug=False)
+ handshakes = intf.operational.get_latest_handshakes()
+
+ # WireGuard performs a handshake every WIREGUARD_REKEY_AFTER_TIME
+ # if data is being transmitted between the peers. If no data is
+ # transmitted, the handshake will not be initiated unless new
+ # data begins to flow. Each handshake generates a new session
+ # key, and the key is rotated at least every 120 seconds or
+ # upon data transmission after a prolonged silence.
+ for public_key, handshake_time in handshakes.items():
+ if public_key in check_peer_public_keys and (
+ handshake_time == 0
+ or (now_time - handshake_time > 3*WIREGUARD_REKEY_AFTER_TIME)
+ ):
+ intf.operational.reset_peer(public_key=public_key)
if __name__ == '__main__':
logger.info('VyOS domain resolver')
@@ -231,5 +308,6 @@ if __name__ == '__main__':
while True:
update_fqdn(firewall, 'firewall')
update_fqdn(nat, 'nat')
+ update_remote_group(firewall)
update_interfaces(interfaces, 'interfaces')
time.sleep(timeout)
diff --git a/src/services/vyos-hostsd b/src/services/vyos-hostsd
index 1ba90471e..44f03586c 100755
--- a/src/services/vyos-hostsd
+++ b/src/services/vyos-hostsd
@@ -233,10 +233,7 @@
# }
import os
-import sys
-import time
import json
-import signal
import traceback
import re
import logging
@@ -245,7 +242,6 @@ import zmq
from voluptuous import Schema, MultipleInvalid, Required, Any
from collections import OrderedDict
from vyos.utils.file import makedir
-from vyos.utils.permission import chown
from vyos.utils.permission import chmod_755
from vyos.utils.process import popen
from vyos.utils.process import process_named_running
diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server
index 558561182..be3dd5051 100755
--- a/src/services/vyos-http-api-server
+++ b/src/services/vyos-http-api-server
@@ -20,18 +20,22 @@ import grp
import json
import logging
import signal
+import traceback
from time import sleep
+from typing import Annotated
-from fastapi import FastAPI
+from fastapi import FastAPI, Query
from fastapi.exceptions import RequestValidationError
from uvicorn import Config as UvicornConfig
from uvicorn import Server as UvicornServer
from vyos.configsession import ConfigSession
from vyos.defaults import api_config_state
+from vyos.utils.file import read_file
+from vyos.version import get_version
from api.session import SessionState
-from api.rest.models import error
+from api.rest.models import error, InfoQueryParams, success
CFG_GROUP = 'vyattacfg'
@@ -57,11 +61,49 @@ app = FastAPI(debug=True,
title="VyOS API",
version="0.1.0")
+
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(_request, exc):
return error(400, str(exc.errors()[0]))
+@app.get('/info')
+def info(q: Annotated[InfoQueryParams, Query()]):
+ show_version = q.version
+ show_hostname = q.hostname
+
+ prelogin_file = r'/etc/issue'
+ hostname_file = r'/etc/hostname'
+ default = 'Welcome to VyOS'
+
+ try:
+ res = {
+ 'banner': '',
+ 'hostname': '',
+ 'version': ''
+ }
+ if show_version:
+ res.update(version=get_version())
+
+ if show_hostname:
+ try:
+ hostname = read_file(hostname_file)
+ except Exception:
+ hostname = 'vyos'
+ res.update(hostname=hostname)
+
+ banner = read_file(prelogin_file, defaultonfailure=default)
+ if banner == f'{default} - \\n \\l':
+ banner = banner.partition(default)[1]
+
+ res.update(banner=banner)
+ except Exception:
+ LOG.critical(traceback.format_exc())
+ return error(500, 'An internal error occured. Check the logs for details.')
+
+ return success(res)
+
+
###
# Modify uvicorn to allow reloading server within the configsession
###
diff --git a/src/system/sync-dhcp-lease-to-hosts.py b/src/system/sync-dhcp-lease-to-hosts.py
new file mode 100755
index 000000000..5c8b18faf
--- /dev/null
+++ b/src/system/sync-dhcp-lease-to-hosts.py
@@ -0,0 +1,112 @@
+#!/usr/bin/env python3
+#
+# Copyright (C) 2025 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 argparse
+import logging
+
+import vyos.opmode
+import vyos.hostsd_client
+
+from vyos.configquery import ConfigTreeQuery
+
+from vyos.kea import kea_get_active_config
+from vyos.kea import kea_get_dhcp_pools
+from vyos.kea import kea_get_server_leases
+
+# Configure logging
+logger = logging.getLogger(__name__)
+# set stream as output
+logs_handler = logging.StreamHandler()
+logger.addHandler(logs_handler)
+
+
+def _get_all_server_leases(inet_suffix='4') -> list:
+ mappings = []
+ try:
+ active_config = kea_get_active_config(inet_suffix)
+ except Exception:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server configuration')
+
+ try:
+ pools = kea_get_dhcp_pools(active_config, inet_suffix)
+ mappings = kea_get_server_leases(
+ active_config, inet_suffix, pools, state=[], origin=None
+ )
+ except Exception:
+ raise vyos.opmode.DataUnavailable('Cannot fetch DHCP server leases')
+
+ return mappings
+
+
+if __name__ == '__main__':
+ # Parse command arguments
+ parser = argparse.ArgumentParser()
+ group = parser.add_mutually_exclusive_group(required=True)
+ group.add_argument('--inet', action='store_true', help='Use IPv4 DHCP leases')
+ group.add_argument('--inet6', action='store_true', help='Use IPv6 DHCP leases')
+ args = parser.parse_args()
+
+ inet_suffix = '4' if args.inet else '6'
+ service_suffix = '' if args.inet else 'v6'
+
+ if inet_suffix == '6':
+ raise vyos.opmode.UnsupportedOperation(
+ 'Syncing IPv6 DHCP leases are not supported yet'
+ )
+
+ # Load configuration
+ config = ConfigTreeQuery()
+
+ # Check if DHCP server is configured
+ # Using warning instead of error since this check may fail during first-time
+ # DHCP server setup when the service is not yet configured in the config tree.
+ # This happens when called from systemd's ExecStartPost the first time.
+ if not config.exists(f'service dhcp{service_suffix}-server'):
+ logger.warning(f'DHCP{service_suffix} server is not configured')
+
+ # Check if hostfile-update is enabled
+ if not config.exists(f'service dhcp{service_suffix}-server hostfile-update'):
+ logger.debug(
+ f'Hostfile update is disabled for DHCP{service_suffix} server, skipping hosts update'
+ )
+ exit(0)
+
+ lease_data = _get_all_server_leases(inet_suffix)
+
+ try:
+ hc = vyos.hostsd_client.Client()
+
+ for mapping in lease_data:
+ ip_addr = mapping.get('ip')
+ mac_addr = mapping.get('mac')
+ name = mapping.get('hostname')
+ name = name if name else f'host-{mac_addr.replace(":", "-")}'
+ domain = mapping.get('domain')
+ fqdn = f'{name}.{domain}' if domain else name
+ hc.add_hosts(
+ {
+ f'dhcp-server-{ip_addr}': {
+ fqdn: {'address': [ip_addr], 'aliases': []}
+ }
+ }
+ )
+
+ hc.apply()
+
+ logger.debug('Hosts store updated successfully')
+
+ except vyos.hostsd_client.VyOSHostsdError as e:
+ raise vyos.opmode.InternalError(str(e))
diff --git a/src/systemd/netplug.service b/src/systemd/netplug.service
new file mode 100644
index 000000000..928c553e8
--- /dev/null
+++ b/src/systemd/netplug.service
@@ -0,0 +1,9 @@
+[Unit]
+Description=Network cable hotplug management daemon
+Documentation=man:netplugd(8)
+After=vyos-router.service
+
+[Service]
+Type=forking
+PIDFile=/run/netplugd.pid
+ExecStart=/sbin/netplugd -c /etc/netplug/netplugd.conf -p /run/netplugd.pid
diff --git a/src/systemd/vyos-commitd.service b/src/systemd/vyos-commitd.service
new file mode 100644
index 000000000..5b083f500
--- /dev/null
+++ b/src/systemd/vyos-commitd.service
@@ -0,0 +1,27 @@
+[Unit]
+Description=VyOS commit daemon
+
+# Without this option, lots of default dependencies are added,
+# among them network.target, which creates a dependency cycle
+DefaultDependencies=no
+
+# Seemingly sensible way to say "as early as the system is ready"
+# All vyos-configd needs is read/write mounted root
+After=systemd-remount-fs.service
+Before=vyos-router.service
+
+[Service]
+ExecStart=/usr/bin/python3 -u /usr/libexec/vyos/services/vyos-commitd
+Type=idle
+
+SyslogIdentifier=vyos-commitd
+SyslogFacility=daemon
+
+Restart=on-failure
+
+# Does't work in Jessie but leave it here
+User=root
+Group=vyattacfg
+
+[Install]
+WantedBy=vyos.target
diff --git a/src/systemd/vyos-wan-load-balance.service b/src/systemd/vyos-wan-load-balance.service
index 7d62a2ff6..a59f2c3ae 100644
--- a/src/systemd/vyos-wan-load-balance.service
+++ b/src/systemd/vyos-wan-load-balance.service
@@ -1,15 +1,11 @@
[Unit]
-Description=VyOS WAN load-balancing service
+Description=VyOS WAN Load Balancer
After=vyos-router.service
[Service]
-ExecStart=/opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid
-ExecReload=/bin/kill -s SIGTERM $MAINPID && sleep 5 && /opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid
-ExecStop=/bin/kill -s SIGTERM $MAINPID
-PIDFile=/var/run/vyatta/wlb.pid
-KillMode=process
-Restart=on-failure
-RestartSec=5s
+Type=simple
+Restart=always
+ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-load-balancer.py
[Install]
WantedBy=multi-user.target
diff --git a/src/systemd/vyos.target b/src/systemd/vyos.target
index 47c91c1cc..ea1593fe9 100644
--- a/src/systemd/vyos.target
+++ b/src/systemd/vyos.target
@@ -1,3 +1,3 @@
[Unit]
Description=VyOS target
-After=multi-user.target
+After=multi-user.target vyos-grub-update.service systemd-sysctl.service
diff --git a/src/tests/test_config_diff.py b/src/tests/test_config_diff.py
index 39e17613a..4017fff4d 100644
--- a/src/tests/test_config_diff.py
+++ b/src/tests/test_config_diff.py
@@ -31,11 +31,11 @@ class TestConfigDiff(TestCase):
def test_unit(self):
diff = vyos.configtree.DiffTree(self.config_left, self.config_null)
sub = diff.sub
- self.assertEqual(sub.to_string(), self.config_left.to_string())
+ self.assertEqual(sub, self.config_left)
diff = vyos.configtree.DiffTree(self.config_null, self.config_left)
add = diff.add
- self.assertEqual(add.to_string(), self.config_left.to_string())
+ self.assertEqual(add, self.config_left)
def test_symmetry(self):
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -45,10 +45,10 @@ class TestConfigDiff(TestCase):
sub = lr_diff.sub
add = rl_diff.add
- self.assertEqual(sub.to_string(), add.to_string())
+ self.assertEqual(sub, add)
add = lr_diff.add
sub = rl_diff.sub
- self.assertEqual(add.to_string(), sub.to_string())
+ self.assertEqual(add, sub)
def test_identity(self):
lr_diff = vyos.configtree.DiffTree(self.config_left,
@@ -61,6 +61,9 @@ class TestConfigDiff(TestCase):
r_union = vyos.configtree.union(add, inter)
l_union = vyos.configtree.union(sub, inter)
+ # here we must compare string representations instead of using
+ # dunder equal, as we assert equivalence of the values list, which
+ # is optionally ordered at render
self.assertEqual(r_union.to_string(),
self.config_right.to_string(ordered_values=True))
self.assertEqual(l_union.to_string(),
diff --git a/src/tests/test_config_parser.py b/src/tests/test_config_parser.py
index 9a4f02859..1b4a57311 100644
--- a/src/tests/test_config_parser.py
+++ b/src/tests/test_config_parser.py
@@ -51,3 +51,7 @@ class TestConfigParser(TestCase):
def test_rename_duplicate(self):
with self.assertRaises(vyos.configtree.ConfigTreeError):
self.config.rename(["top-level-tag-node", "foo"], "bar")
+
+ def test_leading_slashes(self):
+ self.assertTrue(self.config.exists(["normal-node", "value-with-leading-slashes"]))
+ self.assertEqual(self.config.return_value(["normal-node", "value-with-leading-slashes"]), "//other-value")
diff --git a/src/tests/test_template.py b/src/tests/test_template.py
index 6377f6da5..7cae867a0 100644
--- a/src/tests/test_template.py
+++ b/src/tests/test_template.py
@@ -190,3 +190,12 @@ class TestVyOSTemplate(TestCase):
for group_name, group_config in data['ike_group'].items():
ciphers = vyos.template.get_esp_ike_cipher(group_config)
self.assertIn(IKEv2_DEFAULT, ','.join(ciphers))
+
+ def test_get_default_port(self):
+ from vyos.defaults import internal_ports
+
+ with self.assertRaises(RuntimeError):
+ vyos.template.get_default_port('UNKNOWN')
+
+ self.assertEqual(vyos.template.get_default_port('certbot_haproxy'),
+ internal_ports['certbot_haproxy'])
diff --git a/src/tests/test_utils_network.py b/src/tests/test_utils_network.py
index d68dec16f..92fde447d 100644
--- a/src/tests/test_utils_network.py
+++ b/src/tests/test_utils_network.py
@@ -1,4 +1,4 @@
-# Copyright (C) 2020-2024 VyOS maintainers and contributors
+# Copyright (C) 2020-2025 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,3 +43,12 @@ class TestVyOSUtilsNetwork(TestCase):
self.assertFalse(vyos.utils.network.is_loopback_addr('::2'))
self.assertFalse(vyos.utils.network.is_loopback_addr('192.0.2.1'))
+
+ def test_check_port_availability(self):
+ self.assertTrue(vyos.utils.network.check_port_availability('::1', 8080))
+ self.assertTrue(vyos.utils.network.check_port_availability('127.0.0.1', 8080))
+ self.assertTrue(vyos.utils.network.check_port_availability(None, 8080, protocol='udp'))
+ # We do not have 192.0.2.1 configured on this system
+ self.assertFalse(vyos.utils.network.check_port_availability('192.0.2.1', 443))
+ # We do not have 2001:db8::1 configured on this system
+ self.assertFalse(vyos.utils.network.check_port_availability('2001:db8::1', 80, protocol='udp'))
diff --git a/src/validators/base64 b/src/validators/base64
index e2b1e730d..a54168ef7 100755
--- a/src/validators/base64
+++ b/src/validators/base64
@@ -1,6 +1,6 @@
#!/usr/bin/env python3
#
-# Copyright (C) 2021 VyOS maintainers and contributors
+# Copyright (C) 2021-2025 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,13 +15,17 @@
# along with this program. If not, see <http://www.gnu.org/licenses/>.
import base64
-from sys import argv
+import argparse
-if __name__ == '__main__':
- if len(argv) != 2:
- exit(1)
- try:
- base64.b64decode(argv[1])
- except:
+parser = argparse.ArgumentParser(description="Validate base64 input.")
+parser.add_argument("base64", help="Base64 encoded string to validate")
+parser.add_argument("--decoded-len", type=int, help="Optional list of valid lengths for the decoded input")
+args = parser.parse_args()
+
+try:
+ decoded = base64.b64decode(args.base64)
+ if args.decoded_len and len(decoded) != args.decoded_len:
exit(1)
- exit(0)
+except:
+ exit(1)
+exit(0)
diff --git a/src/validators/cpu b/src/validators/cpu
new file mode 100755
index 000000000..959a49248
--- /dev/null
+++ b/src/validators/cpu
@@ -0,0 +1,43 @@
+#!/usr/bin/python3
+
+import re
+import sys
+
+MAX_CPU = 511
+
+
+def validate_isolcpus(value):
+ pattern = re.compile(r'^(\d{1,3}(-\d{1,3})?)(,(\d{1,3}(-\d{1,3})?))*$')
+ if not pattern.fullmatch(value):
+ return False
+
+ flat_list = []
+ for part in value.split(','):
+ if '-' in part:
+ start, end = map(int, part.split('-'))
+ if start > end or start < 0 or end > MAX_CPU:
+ return False
+ flat_list.extend(range(start, end + 1))
+ else:
+ num = int(part)
+ if num < 0 or num > MAX_CPU:
+ return False
+ flat_list.append(num)
+
+ for i in range(1, len(flat_list)):
+ if flat_list[i] <= flat_list[i - 1]:
+ return False
+
+ return True
+
+
+if __name__ == "__main__":
+ if len(sys.argv) != 2:
+ print("Usage: python3 cpu.py <cpu_list>")
+ sys.exit(1)
+
+ input_value = sys.argv[1]
+ if validate_isolcpus(input_value):
+ sys.exit(0)
+ else:
+ sys.exit(1)
diff --git a/src/validators/ethernet-interface b/src/validators/ethernet-interface
new file mode 100644
index 000000000..2bf92812e
--- /dev/null
+++ b/src/validators/ethernet-interface
@@ -0,0 +1,13 @@
+#!/bin/sh
+
+if ! [[ "$1" =~ ^(lan|eth|eno|ens|enp|enx)[0-9]+$ ]]; then
+ echo "Error: $1 is not an ethernet interface"
+ exit 1
+fi
+
+if ! [ -d "/sys/class/net/$1" ]; then
+ echo "Error: $1 interface does not exist in the system"
+ exit 1
+fi
+
+exit 0
diff --git a/tests/data/config.valid b/tests/data/config.valid
index 1fbdd1505..024e5e05c 100644
--- a/tests/data/config.valid
+++ b/tests/data/config.valid
@@ -26,6 +26,7 @@ normal-node {
}
}
option-with-quoted-value "some-value"
+ value-with-leading-slashes "//other-value"
}
trailing-leaf-node-option some-value
@@ -35,5 +36,5 @@ empty-node {
trailing-leaf-node-without-value
-// Trailing comment
-// Another trailing comment
+// some version string info
+// continued