diff options
80 files changed, 1405 insertions, 294 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 933894447..cd348ead7 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -18,8 +18,8 @@ the box, please use [x] - [ ] Other (please describe): ## Related Task(s) -<!-- All submitted PRs must be linked to a Task on Phabricator. --> -* https://vyos.dev/Txxxx +<!-- optional: Link to related other tasks on Phabricator. --> +<!-- * https://vyos.dev/Txxxx --> ## Related PR(s) <!-- Link here any PRs in other repositories that are required by this PR --> diff --git a/data/templates/conntrack/nftables-helpers.j2 b/data/templates/conntrack/nftables-helpers.j2 index 433931162..63a0cc855 100644 --- a/data/templates/conntrack/nftables-helpers.j2 +++ b/data/templates/conntrack/nftables-helpers.j2 @@ -31,6 +31,12 @@ } {% endif %} +{% if modules.rtsp is vyos_defined and ipv4 %} + ct helper rtsp_tcp { + type "rtsp" protocol tcp; + } +{% endif %} + {% if modules.sip is vyos_defined %} ct helper sip_tcp { type "sip" protocol tcp; diff --git a/data/templates/dhcp-client/override.conf.j2 b/data/templates/dhcp-client/override.conf.j2 index d09320270..c2e059c7b 100644 --- a/data/templates/dhcp-client/override.conf.j2 +++ b/data/templates/dhcp-client/override.conf.j2 @@ -3,9 +3,6 @@ {% set if_metric = '-e IF_METRIC=' ~ dhcp_options.default_route_distance if dhcp_options.default_route_distance is vyos_defined else '' %} {% set dhclient_options = '-d -nw -cf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.conf -pf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.pid -lf ' ~ isc_dhclient_dir ~ '/dhclient_' ~ ifname ~ '.leases ' ~ if_metric %} -[Unit] -ConditionPathExists={{ isc_dhclient_dir }}/dhclient_%i.conf - [Service] ExecStart= ExecStart={{ vrf_command }}/sbin/dhclient -4 {{ dhclient_options }} {{ ifname }} diff --git a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 index 74c63a7a0..b37cf4798 100644 --- a/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 +++ b/data/templates/dhcp-server/kea-ctrl-agent.conf.j2 @@ -1,7 +1,7 @@ { "Control-agent": { -{% if failover is vyos_defined %} - "http-host": "{{ failover.source_address }}", +{% if high_availability is vyos_defined %} + "http-host": "{{ high_availability.source_address }}", "http-port": 647, "control-sockets": { "dhcp4": { diff --git a/data/templates/dhcp-server/kea-dhcp4.conf.j2 b/data/templates/dhcp-server/kea-dhcp4.conf.j2 index 629fa952a..bf37b94f6 100644 --- a/data/templates/dhcp-server/kea-dhcp4.conf.j2 +++ b/data/templates/dhcp-server/kea-dhcp4.conf.j2 @@ -51,11 +51,11 @@ } ], "hooks-libraries": [ -{% if failover is vyos_defined %} +{% if high_availability is vyos_defined %} { "library": "/usr/lib/{{ machine }}-linux-gnu/kea/hooks/libdhcp_ha.so", "parameters": { - "high-availability": [{{ failover | kea_failover_json }}] + "high-availability": [{{ high_availability | kea_high_availability_json }}] } }, {% endif %} diff --git a/data/templates/router-advert/radvd.conf.j2 b/data/templates/router-advert/radvd.conf.j2 index 4ef4751dd..97180d164 100644 --- a/data/templates/router-advert/radvd.conf.j2 +++ b/data/templates/router-advert/radvd.conf.j2 @@ -50,6 +50,13 @@ interface {{ iface }} { {% endfor %} }; {% endif %} +{% if iface_config.nat64prefix is vyos_defined %} +{% for nat64prefix, nat64prefix_options in iface_config.nat64prefix.items() %} + nat64prefix {{ nat64prefix }} { + AdvValidLifetime {{ nat64prefix_options.valid_lifetime }}; + }; +{% endfor %} +{% endif %} {% if iface_config.prefix is vyos_defined %} {% for prefix, prefix_options in iface_config.prefix.items() %} prefix {{ prefix }} { diff --git a/debian/control b/debian/control index dddc4e14c..c5a60f660 100644 --- a/debian/control +++ b/debian/control @@ -256,6 +256,9 @@ Depends: # For "nat64" jool, # End "nat64" +# For "system conntrack modules rtsp" + nat-rtsp, +# End "system conntrack modules rtsp" # For "system ntp" chrony, # End "system ntp" diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index f0db8a6f2..7e1f4811a 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -128,7 +128,17 @@ </leafNode> <leafNode name="image"> <properties> - <help>Image name in the hub-registry</help> + <help>Container image to use</help> + <completionHelp> + <script>sudo podman image list --format "{{.Repository}}:{{.Tag}}"</script> + </completionHelp> + <valueHelp> + <format>txt</format> + <description>Image name in the hub-registry</description> + </valueHelp> + <constraint> + <regex>[[:ascii:]]{1,255}</regex> + </constraint> </properties> </leafNode> <leafNode name="command"> @@ -165,6 +175,9 @@ <format>txt</format> <description>Set label option value</description> </valueHelp> + <constraint> + <regex>[[:ascii:]]{1,255}</regex> + </constraint> </properties> </leafNode> </children> @@ -211,6 +224,7 @@ <completionHelp> <path>container network</path> </completionHelp> + #include <include/constraint/container-network.xml.i> </properties> <children> <leafNode name="address"> @@ -426,10 +440,7 @@ <tagNode name="network"> <properties> <help>Network name</help> - <constraint> - <regex>[-_a-zA-Z0-9]{1,11}</regex> - </constraint> - <constraintErrorMessage>Network name cannot be longer than 11 characters</constraintErrorMessage> + #include <include/constraint/container-network.xml.i> </properties> <children> #include <include/generic-description.xml.i> diff --git a/interface-definitions/include/constraint/container-network.xml.i b/interface-definitions/include/constraint/container-network.xml.i new file mode 100644 index 000000000..6f0f06d6f --- /dev/null +++ b/interface-definitions/include/constraint/container-network.xml.i @@ -0,0 +1,6 @@ +<!-- include start from constraint/container-network.xml.i --> +<constraint> + <regex>[-_a-zA-Z0-9]{1,11}</regex> +</constraint> +<constraintErrorMessage>Network name cannot be longer than 11 characters</constraintErrorMessage> +<!-- include end --> diff --git a/interface-definitions/include/constraint/dhcp-client-string-option.xml.i b/interface-definitions/include/constraint/dhcp-client-string-option.xml.i index 88257a9bb..0e3fb8a96 100644 --- a/interface-definitions/include/constraint/dhcp-client-string-option.xml.i +++ b/interface-definitions/include/constraint/dhcp-client-string-option.xml.i @@ -1,4 +1,4 @@ <!-- include start from constraint/dhcp-client-string-option.xml.i --> -<regex>[-_a-zA-Z0-9\s]+</regex> +<regex>[-_a-zA-Z0-9.\s]+</regex> <regex>([a-fA-F0-9][a-fA-F0-9]:){2,}[a-fA-F0-9][a-fA-F0-9]</regex> <!-- include end --> diff --git a/interface-definitions/include/firewall/conntrack-helper.xml.i b/interface-definitions/include/firewall/conntrack-helper.xml.i index ee17f2c61..3ca1a0353 100644 --- a/interface-definitions/include/firewall/conntrack-helper.xml.i +++ b/interface-definitions/include/firewall/conntrack-helper.xml.i @@ -22,6 +22,10 @@ <description>Related traffic from NFS helper</description> </valueHelp> <valueHelp> + <format>rtsp</format> + <description>Related traffic from RTSP helper</description> + </valueHelp> + <valueHelp> <format>sip</format> <description>Related traffic from SIP helper</description> </valueHelp> @@ -34,7 +38,7 @@ <description>Related traffic from SQLNet helper</description> </valueHelp> <constraint> - <regex>(ftp|h323|pptp|nfs|sip|tftp|sqlnet)</regex> + <regex>(ftp|h323|pptp|nfs|rtsp|sip|tftp|sqlnet)</regex> </constraint> <multi/> </properties> diff --git a/interface-definitions/include/generic-description.xml.i b/interface-definitions/include/generic-description.xml.i index 0b3701534..7e091eae7 100644 --- a/interface-definitions/include/generic-description.xml.i +++ b/interface-definitions/include/generic-description.xml.i @@ -7,7 +7,7 @@ <description>Description</description> </valueHelp> <constraint> - <regex>[[:ascii:]]{0,255}</regex> + <regex>.{0,255}</regex> </constraint> <constraintErrorMessage>Description too long (limit 255 characters)</constraintErrorMessage> </properties> diff --git a/interface-definitions/include/interface/vif-s.xml.i b/interface-definitions/include/interface/vif-s.xml.i index fdd62b63d..02e7ab057 100644 --- a/interface-definitions/include/interface/vif-s.xml.i +++ b/interface-definitions/include/interface/vif-s.xml.i @@ -18,27 +18,7 @@ #include <include/interface/dhcpv6-options.xml.i> #include <include/interface/disable-link-detect.xml.i> #include <include/interface/disable.xml.i> - <leafNode name="protocol"> - <properties> - <help>Protocol used for service VLAN (default: 802.1ad)</help> - <completionHelp> - <list>802.1ad 802.1q</list> - </completionHelp> - <valueHelp> - <format>802.1ad</format> - <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> - </valueHelp> - <valueHelp> - <format>802.1q</format> - <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> - </valueHelp> - <constraint> - <regex>(802.1q|802.1ad)</regex> - </constraint> - <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> - </properties> - <defaultValue>802.1ad</defaultValue> - </leafNode> + #include <include/interface/vlan-protocol.xml.i> #include <include/interface/ipv4-options.xml.i> #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> diff --git a/interface-definitions/include/interface/vlan-protocol.xml.i b/interface-definitions/include/interface/vlan-protocol.xml.i new file mode 100644 index 000000000..2fe8d65d7 --- /dev/null +++ b/interface-definitions/include/interface/vlan-protocol.xml.i @@ -0,0 +1,23 @@ +<!-- include start from interface/vif.xml.i --> +<leafNode name="protocol"> + <properties> + <help>Protocol used for service VLAN (default: 802.1ad)</help> + <completionHelp> + <list>802.1ad 802.1q</list> + </completionHelp> + <valueHelp> + <format>802.1ad</format> + <description>Provider Bridging (IEEE 802.1ad, Q-inQ), ethertype 0x88a8</description> + </valueHelp> + <valueHelp> + <format>802.1q</format> + <description>VLAN-tagged frame (IEEE 802.1q), ethertype 0x8100</description> + </valueHelp> + <constraint> + <regex>(802.1q|802.1ad)</regex> + </constraint> + <constraintErrorMessage>Ethertype must be 802.1ad or 802.1q</constraintErrorMessage> + </properties> + <defaultValue>802.1ad</defaultValue> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/qos/mtu.xml.i b/interface-definitions/include/qos/mtu.xml.i new file mode 100644 index 000000000..161d4c27f --- /dev/null +++ b/interface-definitions/include/qos/mtu.xml.i @@ -0,0 +1,14 @@ +<!-- include start from qos/mtu.xml.i --> +<leafNode name="mtu"> + <properties> + <help>MTU size for this class</help> + <valueHelp> + <format>u32:256-65535</format> + <description>Bytes</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 256-65535"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/version/dhcp-server-version.xml.i b/interface-definitions/include/version/dhcp-server-version.xml.i index 3dcbc513a..71f3d4a36 100644 --- a/interface-definitions/include/version/dhcp-server-version.xml.i +++ b/interface-definitions/include/version/dhcp-server-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/dhcp-server-version.xml.i --> -<syntaxVersion component='dhcp-server' version='10'></syntaxVersion> +<syntaxVersion component='dhcp-server' version='11'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_bridge.xml.in b/interface-definitions/interfaces_bridge.xml.in index d4d277cfc..7fb5f121a 100644 --- a/interface-definitions/interfaces_bridge.xml.in +++ b/interface-definitions/interfaces_bridge.xml.in @@ -98,6 +98,10 @@ <valueless/> </properties> </leafNode> + #include <include/interface/vlan-protocol.xml.i> + <leafNode name="protocol"> + <defaultValue>802.1q</defaultValue> + </leafNode> <leafNode name="max-age"> <properties> <help>Interval at which neighbor bridges are removed</help> diff --git a/interface-definitions/nat64.xml.in b/interface-definitions/nat64.xml.in index dfdd295d2..4b3c157cc 100644 --- a/interface-definitions/nat64.xml.in +++ b/interface-definitions/nat64.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="nat64" owner="${vyos_conf_scripts_dir}/nat64.py"> <properties> - <help>IPv6-to-IPv4 Network Address Translation (NAT64) Settings</help> + <help>Network Address Translation (NAT64) parameters</help> <priority>501</priority> </properties> <children> diff --git a/interface-definitions/nat66.xml.in b/interface-definitions/nat66.xml.in index 1518de8bd..32d501cce 100644 --- a/interface-definitions/nat66.xml.in +++ b/interface-definitions/nat66.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="nat66" owner="${vyos_conf_scripts_dir}/nat66.py"> <properties> - <help>IPv6-to-IPv6 Network Prefix Translation (NAT66/NPT) Settings</help> + <help>Network Prefix Translation (NAT66/NPTv6) parameters</help> <priority>500</priority> </properties> <children> diff --git a/interface-definitions/pki.xml.in b/interface-definitions/pki.xml.in index 7a0b073b4..b922771c1 100644 --- a/interface-definitions/pki.xml.in +++ b/interface-definitions/pki.xml.in @@ -2,7 +2,7 @@ <interfaceDefinition> <node name="pki" owner="${vyos_conf_scripts_dir}/pki.py"> <properties> - <help>VyOS PKI configuration</help> + <help>Public key infrastructure (PKI)</help> <priority>300</priority> </properties> <children> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index 0d82cd3f8..791fa1d87 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -1124,12 +1124,20 @@ <leafNode name="exclude"> <properties> <help>Remove/exclude from the as-path attribute</help> + <completionHelp> + <list>all</list> + </completionHelp> <valueHelp> - <format>u32</format> + <format>u32:1-4294967295</format> <description>AS number</description> </valueHelp> + <valueHelp> + <format>all</format> + <description>Exclude all AS numbers from the as-path</description> + </valueHelp> <constraint> <validator name="as-number-list"/> + <regex>(all)</regex> </constraint> </properties> </leafNode> @@ -1137,7 +1145,7 @@ <properties> <help>Prepend to the as-path</help> <valueHelp> - <format>u32</format> + <format>u32:1-4294967295</format> <description>AS number</description> </valueHelp> <constraint> diff --git a/interface-definitions/qos.xml.in b/interface-definitions/qos.xml.in index 31b9a7d21..7618c3027 100644 --- a/interface-definitions/qos.xml.in +++ b/interface-definitions/qos.xml.in @@ -278,6 +278,7 @@ #include <include/generic-description.xml.i> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> + #include <include/qos/mtu.xml.i> #include <include/qos/class-police-exceed.xml.i> #include <include/qos/class-match.xml.i> #include <include/qos/class-priority.xml.i> @@ -293,6 +294,7 @@ <children> #include <include/qos/bandwidth.xml.i> #include <include/qos/burst.xml.i> + #include <include/qos/mtu.xml.i> #include <include/qos/class-police-exceed.xml.i> </children> </node> diff --git a/interface-definitions/service_config-sync.xml.in b/interface-definitions/service_config-sync.xml.in index 9955acfee..cb51a33b1 100644 --- a/interface-definitions/service_config-sync.xml.in +++ b/interface-definitions/service_config-sync.xml.in @@ -38,11 +38,11 @@ <properties> <help>Connection API timeout</help> <valueHelp> - <format>u32:1-300</format> + <format>u32:1-3600</format> <description>Connection API timeout</description> </valueHelp> <constraint> - <validator name="numeric" argument="--range 1-300"/> + <validator name="numeric" argument="--range 1-3600"/> </constraint> </properties> <defaultValue>60</defaultValue> @@ -73,30 +73,444 @@ </constraint> </properties> </leafNode> - <leafNode name="section"> + <node name="section"> <properties> <help>Section for synchronization</help> - <completionHelp> - <list>nat nat66 firewall</list> - </completionHelp> - <valueHelp> - <format>nat</format> - <description>NAT</description> - </valueHelp> - <valueHelp> - <format>nat66</format> - <description>NAT66</description> - </valueHelp> - <valueHelp> - <format>firewall</format> - <description>firewall</description> - </valueHelp> - <constraint> - <regex>(nat|nat66|firewall)</regex> - </constraint> - <multi/> </properties> - </leafNode> + <children> + <leafNode name="firewall"> + <properties> + <help>Firewall</help> + <valueless/> + </properties> + </leafNode> + <node name="interfaces"> + <properties> + <help>Interfaces</help> + </properties> + <children> + <leafNode name="bonding"> + <properties> + <help>Bonding interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="bridge"> + <properties> + <help>Bridge interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dummy"> + <properties> + <help>Dummy interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ethernet"> + <properties> + <help>Ethernet interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="geneve"> + <properties> + <help>GENEVE interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="input"> + <properties> + <help>Input interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="l2tpv3"> + <properties> + <help>L2TPv3 interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="loopback"> + <properties> + <help>Loopback interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="macsec"> + <properties> + <help>MACsec interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="openvpn"> + <properties> + <help>OpenVPN interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pppoe"> + <properties> + <help>PPPoE interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pseudo-ethernet"> + <properties> + <help>Pseudo-Ethernet interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="sstpc"> + <properties> + <help>SSTP client interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="tunnel"> + <properties> + <help>Tunnel interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="virtual-ethernet"> + <properties> + <help>Virtual Ethernet interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="vti"> + <properties> + <help>Virtual tunnel interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="vxlan"> + <properties> + <help>VXLAN interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="wireguard"> + <properties> + <help>Wireguard interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="wireless"> + <properties> + <help>Wireless interface</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="wwan"> + <properties> + <help>WWAN interface</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="nat"> + <properties> + <help>NAT</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="nat66"> + <properties> + <help>NAT66</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pki"> + <properties> + <help>Public key infrastructure (PKI)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="policy"> + <properties> + <help>Routing policy</help> + <valueless/> + </properties> + </leafNode> + <node name="protocols"> + <properties> + <help>Routing protocols</help> + </properties> + <children> + <leafNode name="babel"> + <properties> + <help>Babel Routing Protocol</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="bfd"> + <properties> + <help>Bidirectional Forwarding Detection (BFD)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="bgp"> + <properties> + <help>Border Gateway Protocol (BGP)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="failover"> + <properties> + <help>Failover route</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="igmp-proxy"> + <properties> + <help>Internet Group Management Protocol (IGMP) proxy</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="isis"> + <properties> + <help>Intermediate System to Intermediate System (IS-IS)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="mpls"> + <properties> + <help>Multiprotocol Label Switching (MPLS)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="nhrp"> + <properties> + <help>Next Hop Resolution Protocol (NHRP) parameters</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ospf"> + <properties> + <help>Open Shortest Path First (OSPF)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ospfv3"> + <properties> + <help>Open Shortest Path First (OSPF) for IPv6</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pim"> + <properties> + <help>Protocol Independent Multicast (PIM) and IGMP</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="pim6"> + <properties> + <help>Protocol Independent Multicast for IPv6 (PIMv6) and MLD</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rip"> + <properties> + <help>Routing Information Protocol (RIP) parameters</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ripng"> + <properties> + <help>Routing Information Protocol (RIPng) parameters</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="rpki"> + <properties> + <help>Resource Public Key Infrastructure (RPKI)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="segment-routing"> + <properties> + <help>Segment Routing</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="static"> + <properties> + <help>Static Routing</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="qos"> + <properties> + <help>Quality of Service (QoS)</help> + </properties> + <children> + <leafNode name="interface"> + <properties> + <help>Interface to apply QoS policy</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="policy"> + <properties> + <help>Service Policy definitions</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="service"> + <properties> + <help>System services</help> + </properties> + <children> + <leafNode name="console-server"> + <properties> + <help>Serial Console Server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dhcp-relay"> + <properties> + <help>Host Configuration Protocol (DHCP) relay agent</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dhcp-server"> + <properties> + <help>Dynamic Host Configuration Protocol (DHCP) for DHCP server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dhcpv6-relay"> + <properties> + <help>DHCPv6 Relay Agent parameters</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dhcpv6-server"> + <properties> + <help>DHCP for IPv6 (DHCPv6) server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="dns"> + <properties> + <help>Domain Name System (DNS) related services</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="lldp"> + <properties> + <help>LLDP settings</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="mdns"> + <properties> + <help>Multicast DNS (mDNS) parameters</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="monitoring"> + <properties> + <help>Monitoring services</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ndp-proxy"> + <properties> + <help>Neighbor Discovery Protocol (NDP) Proxy</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="ntp"> + <properties> + <help>Network Time Protocol (NTP) configuration</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="snmp"> + <properties> + <help>Simple Network Management Protocol (SNMP)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="tftp-server"> + <properties> + <help>Trivial File Transfer Protocol (TFTP) server</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="webproxy"> + <properties> + <help>Webproxy service settings</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <node name="system"> + <properties> + <help>System parameters</help> + </properties> + <children> + <leafNode name="conntrack"> + <properties> + <help>Connection Tracking</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="flow-accounting"> + <properties> + <help>Flow accounting</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="option"> + <properties> + <help>System Options</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="sflow"> + <properties> + <help>sFlow</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="static-host-mapping"> + <properties> + <help>Map host names to addresses</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="sysctl"> + <properties> + <help>Configure kernel parameters at runtime</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> + <leafNode name="vpn"> + <properties> + <help>Virtual Private Network (VPN)</help> + <valueless/> + </properties> + </leafNode> + <leafNode name="vrf"> + <properties> + <help>Virtual Routing and Forwarding</help> + <valueless/> + </properties> + </leafNode> + </children> + </node> </children> </node> </children> diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index 1c10a462d..2afa05a8a 100644 --- a/interface-definitions/service_dhcp-server.xml.in +++ b/interface-definitions/service_dhcp-server.xml.in @@ -16,18 +16,18 @@ <valueless/> </properties> </leafNode> - <node name="failover"> + <node name="high-availability"> <properties> - <help>DHCP failover configuration</help> + <help>DHCP high availability configuration</help> </properties> <children> #include <include/source-address-ipv4.xml.i> <leafNode name="remote"> <properties> - <help>IPv4 remote address used for connectio</help> + <help>IPv4 remote address used for connection</help> <valueHelp> <format>ipv4</format> - <description>IPv4 address of failover peer</description> + <description>IPv4 address of high availability peer</description> </valueHelp> <constraint> <validator name="ipv4-address"/> @@ -45,7 +45,7 @@ </leafNode> <leafNode name="status"> <properties> - <help>Failover hierarchy</help> + <help>High availability hierarchy</help> <completionHelp> <list>primary secondary</list> </completionHelp> @@ -60,7 +60,7 @@ <constraint> <regex>(primary|secondary)</regex> </constraint> - <constraintErrorMessage>Invalid DHCP failover peer status</constraintErrorMessage> + <constraintErrorMessage>Invalid DHCP high availability peer status</constraintErrorMessage> </properties> </leafNode> #include <include/pki/ca-certificate.xml.i> diff --git a/interface-definitions/service_router-advert.xml.in b/interface-definitions/service_router-advert.xml.in index 16c29022d..166a4a0cf 100644 --- a/interface-definitions/service_router-advert.xml.in +++ b/interface-definitions/service_router-advert.xml.in @@ -225,6 +225,36 @@ </leafNode> </children> </tagNode> + <tagNode name="nat64prefix"> + <properties> + <help>NAT64 prefix included in the router advertisements</help> + <valueHelp> + <format>ipv6net</format> + <description>IPv6 prefix to be advertized</description> + </valueHelp> + <constraint> + <validator name="ipv6-prefix"/> + </constraint> + </properties> + <children> + <leafNode name="valid-lifetime"> + <properties> + <help>Time in seconds that the prefix will remain valid</help> + <completionHelp> + <list>infinity</list> + </completionHelp> + <valueHelp> + <format>u32:4-65528</format> + <description>Time in seconds that the prefix will remain valid</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 4-65528"/> + </constraint> + </properties> + <defaultValue>65528</defaultValue> + </leafNode> + </children> + </tagNode> <tagNode name="prefix"> <properties> <help>IPv6 prefix to be advertised in Router Advertisements (RAs)</help> diff --git a/interface-definitions/system_conntrack.xml.in b/interface-definitions/system_conntrack.xml.in index a348097cc..219c6e28e 100644 --- a/interface-definitions/system_conntrack.xml.in +++ b/interface-definitions/system_conntrack.xml.in @@ -289,6 +289,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="rtsp"> + <properties> + <help>RTSP connection tracking</help> + <valueless/> + </properties> + </leafNode> <leafNode name="sip"> <properties> <help>SIP connection tracking</help> diff --git a/op-mode-definitions/container.xml.in b/op-mode-definitions/container.xml.in index 4aa13e913..bb6f97b02 100644 --- a/op-mode-definitions/container.xml.in +++ b/op-mode-definitions/container.xml.in @@ -103,12 +103,28 @@ </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_container</command> <children> - <leafNode name="image"> + <node name="json"> + <properties> + <help>Show containers in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_container --raw</command> + </node> + <node name="image"> <properties> <help>Show container image</help> </properties> <command>sudo ${vyos_op_scripts_dir}/container.py show_image</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show container image in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_image --raw</command> + </node> + </children> + </node> <tagNode name="log"> <properties> <help>Show logs from a given container</help> @@ -116,14 +132,25 @@ <path>container name</path> </completionHelp> </properties> + <!-- no admin check --> <command>sudo podman logs --names "$4"</command> </tagNode> - <leafNode name="network"> + <node name="network"> <properties> <help>Show available container networks</help> </properties> + <!-- no admin check --> <command>sudo ${vyos_op_scripts_dir}/container.py show_network</command> - </leafNode> + <children> + <node name="json"> + <properties> + <help>Show available container networks in JSON format</help> + </properties> + <!-- no admin check --> + <command>sudo ${vyos_op_scripts_dir}/container.py show_network --raw</command> + </node> + </children> + </node> </children> </node> <node name="log"> diff --git a/op-mode-definitions/dhcp.xml.in b/op-mode-definitions/dhcp.xml.in index 3c42c8e8f..0b4a05ffe 100644 --- a/op-mode-definitions/dhcp.xml.in +++ b/op-mode-definitions/dhcp.xml.in @@ -293,7 +293,7 @@ <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> - <command>sudo systemctl restart "dhclient@$4.service"</command> + <command>sudo ${vyos_op_scripts_dir}/dhcp.py renew_client_lease --family inet --interface "$4"</command> </tagNode> </children> </node> @@ -309,7 +309,7 @@ <script>${vyos_completion_dir}/list_interfaces</script> </completionHelp> </properties> - <command>sudo systemctl restart "dhcp6c@$4.service"</command> + <command>sudo ${vyos_op_scripts_dir}/dhcp.py renew_client_lease --family inet6 --interface "$4"</command> </tagNode> </children> </node> diff --git a/op-mode-definitions/force-commit-archive.xml.in b/op-mode-definitions/force-commit-archive.xml.in new file mode 100644 index 000000000..162323c20 --- /dev/null +++ b/op-mode-definitions/force-commit-archive.xml.in @@ -0,0 +1,13 @@ +<?xml version="1.0"?> +<interfaceDefinition> + <node name="force"> + <children> + <leafNode name="commit-archive"> + <properties> + <help>Manually archive configuration</help> + </properties> + <command>/usr/bin/config-mgmt</command> + </leafNode> + </children> + </node> +</interfaceDefinition> diff --git a/op-mode-definitions/pki.xml.in b/op-mode-definitions/pki.xml.in index a81c8d4f7..a5e01bade 100644 --- a/op-mode-definitions/pki.xml.in +++ b/op-mode-definitions/pki.xml.in @@ -4,7 +4,7 @@ <children> <node name="pki"> <properties> - <help>Generate PKI certificates and keys</help> + <help>Generate public key infrastructure (PKI) certificates and keys</help> </properties> <children> <node name="ca"> diff --git a/op-mode-definitions/show-log.xml.in b/op-mode-definitions/show-log.xml.in index a6ce04624..e13270364 100644 --- a/op-mode-definitions/show-log.xml.in +++ b/op-mode-definitions/show-log.xml.in @@ -219,7 +219,7 @@ <path>firewall ipv4 forward filter rule</path> </completionHelp> </properties> - <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-FWD-filter-$8-[ADRJC]\]"</command> + <command>journalctl --no-hostname --boot -k | egrep "\[ipv4-FWD-filter-$8-[ADRJCO]\]"</command> </tagNode> </children> </node> @@ -322,7 +322,7 @@ <path>firewall ipv6 forward filter rule</path> </completionHelp> </properties> - <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-FWD-filter-$8-[ADRJC]\]"</command> + <command>journalctl --no-hostname --boot -k | egrep "\[ipv6-FWD-filter-$8-[ADRJCO]\]"</command> </tagNode> </children> </node> diff --git a/python/vyos/config_mgmt.py b/python/vyos/config_mgmt.py index ff078649d..28ccee769 100644 --- a/python/vyos/config_mgmt.py +++ b/python/vyos/config_mgmt.py @@ -132,6 +132,9 @@ class ConfigMgmt: {}).get('source_address', '') if config.exists(['system', 'host-name']): self.hostname = config.return_value(['system', 'host-name']) + if config.exists(['system', 'domain-name']): + tmp = config.return_value(['system', 'domain-name']) + self.hostname += f'.{tmp}' else: self.hostname = 'vyos' diff --git a/python/vyos/configtree.py b/python/vyos/configtree.py index d048901f0..423fe01ed 100644 --- a/python/vyos/configtree.py +++ b/python/vyos/configtree.py @@ -20,10 +20,22 @@ from ctypes import cdll, c_char_p, c_void_p, c_int, c_bool LIBPATH = '/usr/lib/libvyosconfig.so.0' +def replace_backslash(s, search, replace): + """Modify quoted strings containing backslashes not of escape sequences""" + def replace_method(match): + result = match.group().replace(search, replace) + return result + p = re.compile(r'("[^"]*[\\][^"]*"\n|\'[^\']*[\\][^\']*\'\n)') + return p.sub(replace_method, s) + def escape_backslash(string: str) -> str: - """Escape single backslashes in string that are not in escape sequence""" - p = re.compile(r'(?<!\\)[\\](?!b|f|n|r|t|\\[^bfnrt])') - result = p.sub(r'\\\\', string) + """Escape single backslashes in quoted strings""" + result = replace_backslash(string, '\\', '\\\\') + return result + +def unescape_backslash(string: str) -> str: + """Unescape backslashes in quoted strings""" + result = replace_backslash(string, '\\\\', '\\') return result def extract_version(s): @@ -165,11 +177,14 @@ class ConfigTree(object): def to_string(self, ordered_values=False): config_string = self.__to_string(self.__config, ordered_values).decode() + config_string = unescape_backslash(config_string) config_string = "{0}\n{1}".format(config_string, self.__version) return config_string def to_commands(self, op="set"): - return self.__to_commands(self.__config, op.encode()).decode() + commands = self.__to_commands(self.__config, op.encode()).decode() + commands = unescape_backslash(commands) + return commands def to_json(self): return self.__to_json(self.__config).decode() @@ -362,6 +377,7 @@ def show_diff(left, right, path=[], commands=False, libpath=LIBPATH): msg = __get_error().decode() raise ConfigTreeError(msg) + res = unescape_backslash(res) return res def union(left, right, libpath=LIBPATH): diff --git a/python/vyos/configverify.py b/python/vyos/configverify.py index 5d3723876..6508ccdd9 100644 --- a/python/vyos/configverify.py +++ b/python/vyos/configverify.py @@ -269,14 +269,33 @@ def verify_bridge_delete(config): raise ConfigError(f'Interface "{interface}" cannot be deleted as it ' f'is a member of bridge "{bridge_name}"!') -def verify_interface_exists(ifname): +def verify_interface_exists(ifname, warning_only=False): """ Common helper function used by interface implementations to perform - recurring validation if an interface actually exists. + recurring validation if an interface actually exists. We first probe + if the interface is defined on the CLI, if it's not found we try if + it exists at the OS level. """ import os - if not os.path.exists(f'/sys/class/net/{ifname}'): - raise ConfigError(f'Interface "{ifname}" does not exist!') + from vyos.base import Warning + from vyos.configquery import ConfigTreeQuery + from vyos.utils.dict import dict_search_recursive + + # Check if interface is present in CLI config + config = ConfigTreeQuery() + tmp = config.get_config_dict(['interfaces'], get_first_key=True) + if bool(list(dict_search_recursive(tmp, ifname))): + return True + + # Interface not found on CLI, try Linux Kernel + if os.path.exists(f'/sys/class/net/{ifname}'): + return True + + message = f'Interface "{ifname}" does not exist!' + if warning_only: + Warning(message) + return False + raise ConfigError(message) def verify_source_interface(config): """ diff --git a/python/vyos/ethtool.py b/python/vyos/ethtool.py index 473c98d0c..5e241fc08 100644 --- a/python/vyos/ethtool.py +++ b/python/vyos/ethtool.py @@ -24,7 +24,6 @@ from vyos.utils.process import popen _drivers_without_speed_duplex_flow = ['vmxnet3', 'virtio_net', 'xen_netfront', 'iavf', 'ice', 'i40e', 'hv_netvsc', 'veth', 'ixgbevf', 'tun'] -_drivers_without_eee = ['vmxnet3', 'virtio_net', 'xen_netfront', 'hv_netvsc'] class Ethtool: """ @@ -63,8 +62,6 @@ class Ethtool: _auto_negotiation = False _auto_negotiation_supported = None _flow_control = None - _eee = False - _eee_enabled = None def __init__(self, ifname): # Get driver used for interface @@ -118,15 +115,6 @@ class Ethtool: if not bool(err): self._flow_control = loads(out) - # Get current Energy Efficient Ethernet (EEE) settings, but this is - # not supported by all NICs (e.g. vmxnet3 does not support is) - out, _ = popen(f'ethtool --show-eee {ifname}') - if len(out.splitlines()) > 1: - self._eee = True - # read current EEE setting, this returns: - # EEE status: disabled || EEE status: enabled - inactive || EEE status: enabled - active - self._eee_enabled = bool('enabled' in out.splitlines()[1]) - def check_auto_negotiation_supported(self): """ Check if the NIC supports changing auto-negotiation """ return self._auto_negotiation_supported @@ -211,15 +199,3 @@ class Ethtool: 'flow-control settings!') return 'on' if bool(self._flow_control[0]['autonegotiate']) else 'off' - - def check_eee(self): - """ Check if the NIC supports eee """ - if self.get_driver_name() in _drivers_without_eee: - return False - return self._eee - - def get_eee(self): - if self._eee_enabled == None: - raise ValueError('Interface does not support changing '\ - 'EEE settings!') - return self._eee_enabled diff --git a/python/vyos/frr.py b/python/vyos/frr.py index a01d967e4..c3703cbb4 100644 --- a/python/vyos/frr.py +++ b/python/vyos/frr.py @@ -68,6 +68,7 @@ Apply the new configuration: import tempfile import re +from vyos import ConfigError from vyos.utils.permission import chown from vyos.utils.process import cmd from vyos.utils.process import popen @@ -95,6 +96,7 @@ path_config = '/run/frr' default_add_before = r'(ip prefix-list .*|route-map .*|line vty|end)' + class FrrError(Exception): pass @@ -210,13 +212,12 @@ def reload_configuration(config, daemon=None): LOG.debug(f'reload_configuration: Executing command against frr-reload: "{cmd}"') output, code = popen(cmd, stderr=STDOUT) f.close() + for i, e in enumerate(output.split('\n')): LOG.debug(f'frr-reload output: {i:3} {e}') + if code == 1: - raise CommitError('FRR configuration failed while running commit. Please ' \ - 'enable debugging to examine logs.\n\n\n' \ - 'To enable debugging run: "touch /tmp/vyos.frr.debug" ' \ - 'and "sudo systemctl stop vyos-configd"') + raise ConfigError(output) elif code: raise OSError(code, output) @@ -469,17 +470,22 @@ class FRRConfig: # https://github.com/FRRouting/frr/issues/10133 count = 0 count_max = 5 + emsg = '' while count < count_max: count += 1 try: reload_configuration('\n'.join(self.config), daemon=daemon) break + except ConfigError as e: + emsg = str(e) except: # we just need to re-try the commit of the configuration # for the listed FRR issues above pass if count >= count_max: - raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} dameon!') + if emsg: + raise ConfigError(emsg) + raise ConfigurationNotValid(f'Config commit retry counter ({count_max}) exceeded for {daemon} daemon!') # Save configuration to /run/frr/config/frr.conf save_configuration() diff --git a/python/vyos/ifconfig/bridge.py b/python/vyos/ifconfig/bridge.py index b29e71394..7936e3da5 100644 --- a/python/vyos/ifconfig/bridge.py +++ b/python/vyos/ifconfig/bridge.py @@ -1,4 +1,4 @@ -# Copyright 2019-2021 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2019-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -14,12 +14,11 @@ # License along with this library. If not, see <http://www.gnu.org/licenses/>. from netifaces import interfaces -import json from vyos.ifconfig.interface import Interface from vyos.utils.assertion import assert_boolean +from vyos.utils.assertion import assert_list from vyos.utils.assertion import assert_positive -from vyos.utils.process import cmd from vyos.utils.dict import dict_search from vyos.configdict import get_vlan_ids from vyos.configdict import list_diff @@ -86,6 +85,10 @@ class BridgeIf(Interface): 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/vlan_filtering', }, + 'vlan_protocol': { + 'validate': lambda v: assert_list(v, ['0x88a8', '0x8100']), + 'location': '/sys/class/net/{ifname}/bridge/vlan_protocol', + }, 'multicast_querier': { 'validate': assert_boolean, 'location': '/sys/class/net/{ifname}/bridge/multicast_querier', @@ -248,6 +251,26 @@ class BridgeIf(Interface): """ return self.set_interface('del_port', interface) + def set_vlan_protocol(self, protocol): + """ + Set protocol used for VLAN filtering. + The valid values are 0x8100(802.1q) or 0x88A8(802.1ad). + + Example: + >>> from vyos.ifconfig import Interface + >>> BridgeIf('br0').del_port('eth1') + """ + + if protocol not in ['802.1q', '802.1ad']: + raise ValueError() + + map = { + '802.1ad': '0x88a8', + '802.1q' : '0x8100' + } + + return self.set_interface('vlan_protocol', map[protocol]) + def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -294,10 +317,13 @@ class BridgeIf(Interface): if member in interfaces(): self.del_port(member) - # enable/disable Vlan Filter + # enable/disable VLAN Filter tmp = '1' if 'enable_vlan' in config else '0' self.set_vlan_filter(tmp) + tmp = config.get('protocol') + self.set_vlan_protocol(tmp) + # add VLAN interfaces to local 'parent' bridge to allow forwarding if 'enable_vlan' in config: for vlan in config.get('vif_remove', {}): diff --git a/python/vyos/ifconfig/ethernet.py b/python/vyos/ifconfig/ethernet.py index c3f5bbf47..8d96c863f 100644 --- a/python/vyos/ifconfig/ethernet.py +++ b/python/vyos/ifconfig/ethernet.py @@ -404,34 +404,6 @@ class EthernetIf(Interface): print(f'could not set "{rx_tx}" ring-buffer for {ifname}') return output - def set_eee(self, enable): - """ - Enable/Disable Energy Efficient Ethernet (EEE) settings - - Example: - >>> from vyos.ifconfig import EthernetIf - >>> i = EthernetIf('eth0') - >>> i.set_eee(enable=False) - """ - if not isinstance(enable, bool): - raise ValueError('Value out of range') - - if not self.ethtool.check_eee(): - self._debug_msg(f'NIC driver does not support changing EEE settings!') - return False - - current = self.ethtool.get_eee() - if current != enable: - # Assemble command executed on system. Unfortunately there is no way - # to change this setting via sysfs - cmd = f'ethtool --set-eee {self.ifname} eee ' - cmd += 'on' if enable else 'off' - output, code = self._popen(cmd) - if code: - Warning(f'could not change "{self.ifname}" EEE setting!') - return output - return None - def update(self, config): """ General helper function which works on a dictionary retrived by get_config_dict(). It's main intention is to consolidate the scattered @@ -442,9 +414,6 @@ class EthernetIf(Interface): value = 'off' if 'disable_flow_control' in config else 'on' self.set_flow_control(value) - # Always disable Energy Efficient Ethernet - self.set_eee(False) - # GRO (generic receive offload) self.set_gro(dict_search('offload.gro', config) != None) diff --git a/python/vyos/ifconfig/vti.py b/python/vyos/ifconfig/vti.py index 9ebbeb9ed..9511386f4 100644 --- a/python/vyos/ifconfig/vti.py +++ b/python/vyos/ifconfig/vti.py @@ -1,4 +1,4 @@ -# Copyright 2021-2022 VyOS maintainers and contributors <maintainers@vyos.io> +# Copyright 2021-2024 VyOS maintainers and contributors <maintainers@vyos.io> # # This library is free software; you can redistribute it and/or # modify it under the terms of the GNU Lesser General Public @@ -52,8 +52,14 @@ class VTIIf(Interface): cmd += f' {iproute2_key} {tmp}' self._cmd(cmd.format(**self.config)) + + # interface is always A/D down. It needs to be enabled explicitly self.set_interface('admin_state', 'down') + def set_admin_state(self, state): + """ Handled outside by /etc/ipsec.d/vti-up-down """ + pass + def get_mac(self): """ Get a synthetic MAC address. """ return self.get_mac_synthetic() diff --git a/python/vyos/opmode.py b/python/vyos/opmode.py index e1af1a682..8dab9a4ca 100644 --- a/python/vyos/opmode.py +++ b/python/vyos/opmode.py @@ -81,7 +81,7 @@ class InternalError(Error): def _is_op_mode_function_name(name): - if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set)", name): + if re.match(r"^(show|clear|reset|restart|add|update|delete|generate|set|renew)", name): return True else: return False diff --git a/python/vyos/priority.py b/python/vyos/priority.py new file mode 100644 index 000000000..ab4e6d411 --- /dev/null +++ b/python/vyos/priority.py @@ -0,0 +1,75 @@ +# Copyright 2024 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from pathlib import Path +from typing import List + +from vyos.xml_ref import load_reference +from vyos.base import Warning as Warn + +def priority_data(d: dict) -> list: + def func(d, path, res, hier): + for k,v in d.items(): + if not 'node_data' in v: + continue + subpath = path + [k] + hier_prio = hier + data = v.get('node_data') + o = data.get('owner') + p = data.get('priority') + # a few interface-definitions have priority preceding owner + # attribute, instead of within properties; pass in descent + if p is not None and o is None: + hier_prio = p + if o is not None and p is None: + p = hier_prio + if o is not None and p is not None: + o = Path(o.split()[0]).name + p = int(p) + res.append((subpath, o, p)) + if isinstance(v, dict): + func(v, subpath, res, hier_prio) + return res + ret = func(d, [], [], 0) + ret = sorted(ret, key=lambda x: x[0]) + ret = sorted(ret, key=lambda x: x[2]) + return ret + +def get_priority_data() -> list: + xml = load_reference() + return priority_data(xml.ref) + +def priority_sort(sections: List[list[str]] = None, + owners: List[str] = None, + reverse=False) -> List: + if sections is not None: + index = 0 + collection: List = sections + elif owners is not None: + index = 1 + collection = owners + else: + raise ValueError('one of sections or owners is required') + + l = get_priority_data() + m = [item for item in l if item[index] in collection] + n = sorted(m, key=lambda x: x[2], reverse=reverse) + o = [item[index] for item in n] + # sections are unhashable; use comprehension + missed = [j for j in collection if j not in o] + if missed: + Warn(f'No priority available for elements {missed}') + + return o diff --git a/python/vyos/qos/base.py b/python/vyos/qos/base.py index 47318122b..c8e881ee2 100644 --- a/python/vyos/qos/base.py +++ b/python/vyos/qos/base.py @@ -324,6 +324,11 @@ class QoSBase: if 'burst' in cls_config: burst = cls_config['burst'] filter_cmd += f' burst {burst}' + + if 'mtu' in cls_config: + mtu = cls_config['mtu'] + filter_cmd += f' mtu {mtu}' + cls = int(cls) filter_cmd += f' flowid {self._parent:x}:{cls:x}' self._cmd(filter_cmd) @@ -387,6 +392,10 @@ class QoSBase: burst = config['default']['burst'] filter_cmd += f' burst {burst}' + if 'mtu' in config['default']: + mtu = config['default']['mtu'] + filter_cmd += f' mtu {mtu}' + if 'class' in config: filter_cmd += f' flowid {self._parent:x}:{default_cls_id:x}' diff --git a/python/vyos/system/compat.py b/python/vyos/system/compat.py index 37b834ad6..1b487c1d2 100644 --- a/python/vyos/system/compat.py +++ b/python/vyos/system/compat.py @@ -198,11 +198,11 @@ def update_cfg_ver(root_dir:str = '') -> int: return cfg_version -def get_default(menu_entries: list, root_dir: str = '') -> Union[int, None]: +def get_default(data: dict, root_dir: str = '') -> Union[int, None]: """Translate default version to menuentry index Args: - menu_entries (list): list of dicts of installed version boot data + data (dict): boot data root_dir (str): an optional path to the root directory Returns: @@ -213,10 +213,22 @@ def get_default(menu_entries: list, root_dir: str = '') -> Union[int, None]: grub_cfg_main = f'{root_dir}/{grub.GRUB_CFG_MAIN}' + menu_entries = data.get('versions', []) + console_type = data.get('console_type', 'tty') + console_num = data.get('console_num', '0') image_name = image.get_default_image() - sublist = list(filter(lambda x: x.get('version') == image_name, - menu_entries)) + sublist = list(filter(lambda x: (x.get('version') == image_name and + x.get('console_type') == console_type and + x.get('console_num') == console_num and + x.get('bootmode') == 'normal'), + menu_entries)) + # legacy images added with legacy tools omitted 'ttyUSB'; if entry not + # available, default to initial entry of version + if not sublist: + sublist = list(filter(lambda x: x.get('version') == image_name, + menu_entries)) + if sublist: return menu_entries.index(sublist[0]) @@ -291,7 +303,7 @@ def grub_cfg_fields(root_dir: str = '') -> dict: menu_entries = update_version_list(root_dir) fields['versions'] = menu_entries - default = get_default(menu_entries, root_dir) + default = get_default(fields, root_dir) if default is not None: fields['default'] = default diff --git a/python/vyos/system/grub.py b/python/vyos/system/grub.py index 2e8b20972..e56f0bec8 100644 --- a/python/vyos/system/grub.py +++ b/python/vyos/system/grub.py @@ -17,6 +17,7 @@ import platform from pathlib import Path from re import MULTILINE, compile as re_compile +from shutil import copy2 from typing import Union from uuid import uuid5, NAMESPACE_URL, UUID @@ -373,7 +374,7 @@ def create_structure(root_dir: str = '') -> None: if not root_dir: root_dir = disk.find_persistence() - Path(f'{root_dir}/GRUB_DIR_VYOS_VERS').mkdir(parents=True, exist_ok=True) + Path(f'{root_dir}/{GRUB_DIR_VYOS_VERS}').mkdir(parents=True, exist_ok=True) def set_console_type(console_type: str, root_dir: str = '') -> None: @@ -422,3 +423,38 @@ def set_kernel_cmdline_options(cmdline_options: str, version_name: str, version_add(version_name=version_name, root_dir=root_dir, boot_opts_config=cmdline_options) + + +def sort_inodes(dir_path: str) -> None: + """Sort inodes for files inside a folder + Regenerate inodes for each file to get the same order for both inodes + and file names + + GRUB iterates files by inodes, not alphabetically. Therefore, if we + want to read them in proper order, we need to sort inodes for all + config files in a folder. + + Args: + dir_path (str): a path to directory + """ + dir_content: list[Path] = sorted(Path(dir_path).iterdir()) + temp_list_old: list[Path] = [] + temp_list_new: list[Path] = [] + + # create a copy of all files, to get new inodes + for item in dir_content: + # skip directories + if item.is_dir(): + continue + # create a new copy of file with a temporary name + copy_path = Path(f'{item.as_posix()}_tmp') + copy2(item, Path(copy_path)) + temp_list_old.append(item) + temp_list_new.append(copy_path) + + # delete old files and rename new ones + for item in temp_list_old: + item.unlink() + for item in temp_list_new: + new_name = Path(f'{item.as_posix()[0:-4]}') + item.rename(new_name) diff --git a/python/vyos/template.py b/python/vyos/template.py index 456239568..bde8e3554 100644 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -803,8 +803,8 @@ def kea_address_json(addresses): return dumps(out) -@register_filter('kea_failover_json') -def kea_failover_json(config): +@register_filter('kea_high_availability_json') +def kea_high_availability_json(config): from json import dumps source_addr = config['source_address'] diff --git a/python/vyos/xml_ref/generate_cache.py b/python/vyos/xml_ref/generate_cache.py index 6a05d4608..d1ccb0f81 100755 --- a/python/vyos/xml_ref/generate_cache.py +++ b/python/vyos/xml_ref/generate_cache.py @@ -38,7 +38,8 @@ xml_tmp = join('/tmp', xml_cache_json) pkg_cache = abspath(join(_here, 'pkg_cache')) ref_cache = abspath(join(_here, 'cache.py')) -node_data_fields = ("node_type", "multi", "valueless", "default_value") +node_data_fields = ("node_type", "multi", "valueless", "default_value", + "owner", "priority") def trim_node_data(cache: dict): for k in list(cache): diff --git a/smoketest/config-tests/dialup-router-medium-vpn b/smoketest/config-tests/dialup-router-medium-vpn index bdae6e807..89eec0f65 100644 --- a/smoketest/config-tests/dialup-router-medium-vpn +++ b/smoketest/config-tests/dialup-router-medium-vpn @@ -249,10 +249,10 @@ set service conntrack-sync expect-sync 'all' set service conntrack-sync failover-mechanism vrrp sync-group 'failover-group' set service conntrack-sync interface eth1 peer '192.168.0.251' set service conntrack-sync sync-queue-size '8' -set service dhcp-server failover name 'DHCP02' -set service dhcp-server failover remote '192.168.0.251' -set service dhcp-server failover source-address '192.168.0.250' -set service dhcp-server failover status 'primary' +set service dhcp-server high-availability name 'DHCP02' +set service dhcp-server high-availability remote '192.168.0.251' +set service dhcp-server high-availability source-address '192.168.0.250' +set service dhcp-server high-availability status 'primary' set service dhcp-server shared-network-name LAN authoritative set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 lease '86400' set service dhcp-server shared-network-name LAN subnet 192.168.0.0/24 option default-router '192.168.0.1' diff --git a/smoketest/scripts/cli/test_backslash_escape.py b/smoketest/scripts/cli/test_backslash_escape.py new file mode 100755 index 000000000..e94e9ab0a --- /dev/null +++ b/smoketest/scripts/cli/test_backslash_escape.py @@ -0,0 +1,68 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import unittest + +from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.configtree import ConfigTree + +base_path = ['interfaces', 'ethernet', 'eth0', 'description'] + +cases_word = [r'fo\o', r'fo\\o', r'foço\o', r'foço\\o'] +# legacy CLI output quotes only if whitespace present; this is a notable +# difference that confounds the translation legacy -> modern, hence +# determines the regex used in function replace_backslash +cases_phrase = [r'some fo\o', r'some fo\\o', r'some foço\o', r'some foço\\o'] + +case_save_config = '/tmp/smoketest-case-save' + +class TestBackslashEscape(VyOSUnitTestSHIM.TestCase): + def test_backslash_escape_word(self): + for case in cases_word: + self.cli_set(base_path + [case]) + self.cli_commit() + # save_config tests translation though subsystems: + # legacy output -> config -> configtree -> file + self._session.save_config(case_save_config) + # reload to configtree and confirm: + with open(case_save_config) as f: + config_string = f.read() + ct = ConfigTree(config_string) + res = ct.return_value(base_path) + self.assertEqual(case, res, msg=res) + print(f'description: {res}') + self.cli_delete(base_path) + self.cli_commit() + + def test_backslash_escape_phrase(self): + for case in cases_phrase: + self.cli_set(base_path + [case]) + self.cli_commit() + # save_config tests translation though subsystems: + # legacy output -> config -> configtree -> file + self._session.save_config(case_save_config) + # reload to configtree and confirm: + with open(case_save_config) as f: + config_string = f.read() + ct = ConfigTree(config_string) + res = ct.return_value(base_path) + self.assertEqual(case, res, msg=res) + print(f'description: {res}') + self.cli_delete(base_path) + self.cli_commit() + +if __name__ == '__main__': + unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 9e8473fa4..fe6977252 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -598,14 +598,30 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip6 vyos_filter') - def test_ipv4_state_and_status_rules(self): - name = 'smoketest-state' - interface = 'eth0' - + def test_ipv4_global_state(self): 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']) + self.cli_commit() + + nftables_search = [ + ['jump VYOS_STATE_POLICY'], + ['chain VYOS_STATE_POLICY'], + ['ct state established', 'accept'], + ['ct state invalid', 'drop'], + ['ct state related', 'accept'] + ] + + self.verify_nftables(nftables_search, 'ip 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') + + def test_ipv4_state_and_status_rules(self): + name = 'smoketest-state' + self.cli_set(['firewall', 'ipv4', 'name', name, 'default-action', 'drop']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'name', name, 'rule', '1', 'state', 'established']) @@ -632,12 +648,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): ['ct state new', 'ct status dnat', 'accept'], ['ct state { established, new }', 'ct status snat', 'accept'], ['ct state related', 'ct helper { "ftp", "pptp" }', 'accept'], - ['drop', f'comment "{name} default-action drop"'], - ['jump VYOS_STATE_POLICY'], - ['chain VYOS_STATE_POLICY'], - ['ct state established', 'accept'], - ['ct state invalid', 'drop'], - ['ct state related', 'accept'] + ['drop', f'comment "{name} default-action drop"'] ] self.verify_nftables(nftables_search, 'ip vyos_filter') diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 3500e97d6..124c1fbcb 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -182,6 +182,10 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): for interface in self._interfaces: cost = 1000 priority = 10 + + tmp = get_interface_config(interface) + self.assertEqual('802.1Q', tmp['linkinfo']['info_data']['vlan_protocol']) # default VLAN protocol + for member in self._members: tmp = get_interface_config(member) self.assertEqual(interface, tmp['master']) @@ -442,5 +446,19 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): self.cli_delete(['interfaces', 'tunnel', tunnel_if]) self.cli_delete(['interfaces', 'ethernet', 'eth0', 'address', eth0_addr]) + def test_bridge_vlan_protocol(self): + protocol = '802.1ad' + + # Add member interface to bridge and set VLAN filter + for interface in self._interfaces: + self.cli_set(self._base_path + [interface, 'protocol', protocol]) + + # commit config + self.cli_commit() + + for interface in self._interfaces: + tmp = get_interface_config(interface) + self.assertEqual(protocol, tmp['linkinfo']['info_data']['vlan_protocol']) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_interfaces_vti.py b/smoketest/scripts/cli/test_interfaces_vti.py index 7f13575a3..871ac650b 100755 --- a/smoketest/scripts/cli/test_interfaces_vti.py +++ b/smoketest/scripts/cli/test_interfaces_vti.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -18,6 +18,9 @@ import unittest from base_interfaces_test import BasicInterfaceTest +from vyos.ifconfig import Interface +from vyos.utils.network import is_intf_addr_assigned + class VTIInterfaceTest(BasicInterfaceTest.TestCase): @classmethod def setUpClass(cls): @@ -27,5 +30,19 @@ class VTIInterfaceTest(BasicInterfaceTest.TestCase): # call base-classes classmethod super(VTIInterfaceTest, cls).setUpClass() + def test_add_single_ip_address(self): + addr = '192.0.2.0/31' + for intf in self._interfaces: + self.cli_set(self._base_path + [intf, 'address', addr]) + for option in self._options.get(intf, []): + self.cli_set(self._base_path + [intf] + option.split()) + + self.cli_commit() + + # VTI interface are always down and only brought up by IPSec + for intf in self._interfaces: + self.assertTrue(is_intf_addr_assigned(intf, addr)) + self.assertEqual(Interface(intf).get_admin_state(), 'down') + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index c21d8af4e..ee4445251 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1065,6 +1065,7 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): '20' : { 'action' : 'permit', 'set' : { + 'as-path-exclude' : 'all', 'evpn-gateway-ipv4' : '192.0.2.99', 'evpn-gateway-ipv6' : '2001:db8:f00::1', }, diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 1d68ae08b..5f238b25a 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1236,6 +1236,11 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(f' neighbor {pg_ipv6} activate', afiv6_config) self.assertIn(f' neighbor {pg_ipv6} maximum-prefix {ipv6_max_prefix}', afiv6_config) + def test_bgp_27_route_reflector_client(self): + self.cli_set(base_path + ['peer-group', 'peer1', 'address-family', 'l2vpn-evpn', 'route-reflector-client']) + with self.assertRaises(ConfigSessionError) as e: + self.cli_commit() + 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 aa5f2f38c..0fd18a6da 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -73,6 +73,12 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.cli_commit() self.isis_base_config() + + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected']) + # verify() - Redistribute level-1 or level-2 should be specified + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) self.cli_set(base_path + ['log-adjacency-changes']) diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 82fb96754..1b9cc50fe 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -540,5 +540,25 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): for router_id in router_ids: self.assertIn(f' graceful-restart helper enable {router_id}', frrconfig) + def test_ospf_17_duplicate_area_network(self): + area0 = '0' + area1 = '1' + network = '10.0.0.0/8' + + self.cli_set(base_path + ['area', area0, 'network', network]) + + # we can not have the same network defined on two areas + self.cli_set(base_path + ['area', area1, 'network', network]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['area', area0]) + + self.cli_commit() + + # Verify FRR ospfd configuration + frrconfig = self.getFRRconfig('router ospf', daemon=PROCESS_NAME) + self.assertIn(f'router ospf', frrconfig) + self.assertIn(f' network {network} area {area1}', frrconfig) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_qos.py b/smoketest/scripts/cli/test_qos.py index 81e7326f8..46ef68b1d 100755 --- a/smoketest/scripts/cli/test_qos.py +++ b/smoketest/scripts/cli/test_qos.py @@ -38,6 +38,13 @@ def get_tc_filter_json(interface, direction) -> list: tmp = loads(tmp) return tmp +def get_tc_filter_details(interface, direction) -> list: + # json doesn't contain all params, such as mtu + if direction not in ['ingress', 'egress']: + raise ValueError() + tmp = cmd(f'tc -details filter show dev {interface} {direction}') + return tmp + class TestQoS(VyOSUnitTestSHIM.TestCase): @classmethod def setUpClass(cls): @@ -234,7 +241,12 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): def test_05_limiter(self): qos_config = { '1' : { - 'bandwidth' : '1000000', + 'bandwidth' : '3000000', + 'exceed' : 'pipe', + 'burst' : '100Kb', + 'mtu' : '1600', + 'not-exceed' : 'continue', + 'priority': '15', 'match4' : { 'ssh' : { 'dport' : '22', }, }, @@ -262,6 +274,10 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['interface', interface, 'ingress', policy_name]) # set default bandwidth parameter for all remaining connections self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'bandwidth', '500000']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'burst', '200kb']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'exceed', 'drop']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'mtu', '3000']) + self.cli_set(base_path + ['policy', 'limiter', policy_name, 'default', 'not-exceed', 'ok']) for qos_class, qos_class_config in qos_config.items(): qos_class_base = base_path + ['policy', 'limiter', policy_name, 'class', qos_class] @@ -279,6 +295,21 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): if 'bandwidth' in qos_class_config: self.cli_set(qos_class_base + ['bandwidth', qos_class_config['bandwidth']]) + if 'exceed' in qos_class_config: + self.cli_set(qos_class_base + ['exceed', qos_class_config['exceed']]) + + if 'not-exceed' in qos_class_config: + self.cli_set(qos_class_base + ['not-exceed', qos_class_config['not-exceed']]) + + if 'burst' in qos_class_config: + self.cli_set(qos_class_base + ['burst', qos_class_config['burst']]) + + if 'mtu' in qos_class_config: + self.cli_set(qos_class_base + ['mtu', qos_class_config['mtu']]) + + if 'priority' in qos_class_config: + self.cli_set(qos_class_base + ['priority', qos_class_config['priority']]) + # commit changes self.cli_commit() @@ -303,6 +334,14 @@ class TestQoS(VyOSUnitTestSHIM.TestCase): dport = int(match_config['dport']) self.assertEqual(f'{dport:x}', filter['options']['match']['value']) + tc_details = get_tc_filter_details(interface, 'ingress') + self.assertTrue('filter parent ffff: protocol all pref 20 u32 chain 0' in tc_details) + self.assertTrue('rate 1Gbit burst 15125b mtu 2Kb action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('filter parent ffff: protocol all pref 15 u32 chain 0' in tc_details) + self.assertTrue('rate 3Gbit burst 102000b mtu 1600b action pipe/continue overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('rate 500Mbit burst 204687b mtu 3000b action drop overhead 0b linklayer ethernet' in tc_details) + self.assertTrue('filter parent ffff: protocol all pref 255 basic chain 0' in tc_details) + def test_06_network_emulator(self): policy_type = 'network-emulator' diff --git a/smoketest/scripts/cli/test_service_dhcp-server.py b/smoketest/scripts/cli/test_service_dhcp-server.py index b582a2108..24bd14af2 100755 --- a/smoketest/scripts/cli/test_service_dhcp-server.py +++ b/smoketest/scripts/cli/test_service_dhcp-server.py @@ -673,7 +673,7 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): # Check for running process self.assertTrue(process_named_running(PROCESS_NAME)) - def test_dhcp_failover(self): + def test_dhcp_high_availability(self): shared_net_name = 'FAILOVER' failover_name = 'VyOS-Failover' @@ -695,10 +695,10 @@ class TestServiceDHCPServer(VyOSUnitTestSHIM.TestCase): failover_local = router failover_remote = inc_ip(router, 1) - self.cli_set(base_path + ['failover', 'source-address', failover_local]) - self.cli_set(base_path + ['failover', 'name', failover_name]) - self.cli_set(base_path + ['failover', 'remote', failover_remote]) - self.cli_set(base_path + ['failover', 'status', 'primary']) + 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']) # commit changes self.cli_commit() diff --git a/smoketest/scripts/cli/test_service_router-advert.py b/smoketest/scripts/cli/test_service_router-advert.py index 5fc2019fd..d1ff25a58 100755 --- a/smoketest/scripts/cli/test_service_router-advert.py +++ b/smoketest/scripts/cli/test_service_router-advert.py @@ -195,6 +195,34 @@ class TestServiceRADVD(VyOSUnitTestSHIM.TestCase): for src in ra_src: self.assertIn(f' {src};', config) + def test_nat64prefix(self): + nat64prefix = '64:ff9b::/96' + nat64prefix_invalid = '64:ff9b::/44' + + self.cli_set(base_path + ['nat64prefix', nat64prefix]) + + # and another invalid prefix + # Invalid NAT64 prefix length for "2001:db8::/34", can only be one of: + # /32, /40, /48, /56, /64, /96 + self.cli_set(base_path + ['nat64prefix', nat64prefix_invalid]) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['nat64prefix', nat64prefix_invalid]) + + # NAT64 valid-lifetime must not be smaller then "interval max" + self.cli_set(base_path + ['nat64prefix', nat64prefix, 'valid-lifetime', '500']) + with self.assertRaises(ConfigSessionError): + self.cli_commit() + self.cli_delete(base_path + ['nat64prefix', nat64prefix, 'valid-lifetime']) + + # commit changes + self.cli_commit() + + config = read_file(RADVD_CONF) + + tmp = f'nat64prefix {nat64prefix}' + ' {' + self.assertIn(tmp, config) + self.assertIn('AdvValidLifetime 65528;', config) # default if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_system_conntrack.py b/smoketest/scripts/cli/test_system_conntrack.py index f00626b3d..2d76da145 100755 --- a/smoketest/scripts/cli/test_system_conntrack.py +++ b/smoketest/scripts/cli/test_system_conntrack.py @@ -174,12 +174,16 @@ class TestSystemConntrack(VyOSUnitTestSHIM.TestCase): 'pptp': { 'driver': ['nf_nat_pptp', 'nf_conntrack_pptp'], 'nftables': ['ct helper set "pptp_tcp"'] - }, + }, + 'rtsp': { + 'driver': ['nf_nat_rtsp', 'nf_conntrack_rtsp'], + 'nftables': ['ct helper set "rtsp_tcp"'] + }, 'sip': { 'driver': ['nf_nat_sip', 'nf_conntrack_sip'], 'nftables': ['ct helper set "sip_tcp"', 'ct helper set "sip_udp"'] - }, + }, 'sqlnet': { 'nftables': ['ct helper set "tns_tcp"'] }, diff --git a/src/conf_mode/firewall.py b/src/conf_mode/firewall.py index 3c27655b0..810437dda 100755 --- a/src/conf_mode/firewall.py +++ b/src/conf_mode/firewall.py @@ -268,6 +268,18 @@ def verify_rule(firewall, rule_conf, ipv6): if 'port' in side_conf and dict_search_args(side_conf, 'group', 'port_group'): raise ConfigError(f'{side} port-group and port cannot both be defined') + if 'add_address_to_group' in rule_conf: + for type in ['destination_address', 'source_address']: + if type in rule_conf['add_address_to_group']: + if 'address_group' not in rule_conf['add_address_to_group'][type]: + raise ConfigError(f'Dynamic address group must be defined.') + else: + target = rule_conf['add_address_to_group'][type]['address_group'] + fwall_group = 'ipv6_address_group' if ipv6 else 'address_group' + group_obj = dict_search_args(firewall, 'group', 'dynamic_group', fwall_group, target) + if group_obj is None: + raise ConfigError(f'Invalid dynamic address group on firewall rule') + if 'log_options' in rule_conf: if 'log' not in rule_conf: raise ConfigError('log-options defined, but log is not enable') diff --git a/src/conf_mode/protocols_bgp.py b/src/conf_mode/protocols_bgp.py index d90dfe45b..f1c59cbde 100755 --- a/src/conf_mode/protocols_bgp.py +++ b/src/conf_mode/protocols_bgp.py @@ -457,6 +457,8 @@ def verify(bgp): peer_group_as = dict_search(f'peer_group.{peer_group}.remote_as', bgp) if peer_group_as != None and peer_group_as != 'internal' and peer_group_as != bgp['system_as']: raise ConfigError('route-reflector-client only supported for iBGP peers') + else: + raise ConfigError('route-reflector-client only supported for iBGP peers') # Throw an error if a peer group is not configured for allow range for prefix in dict_search('listen.range', bgp) or []: diff --git a/src/conf_mode/protocols_isis.py b/src/conf_mode/protocols_isis.py index 8d594bb68..6c9925b80 100755 --- a/src/conf_mode/protocols_isis.py +++ b/src/conf_mode/protocols_isis.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2020-2022 VyOS maintainers and contributors +# Copyright (C) 2020-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -155,12 +155,12 @@ def verify(isis): for proto, proto_config in isis['redistribute'][afi].items(): if 'level_1' not in proto_config and 'level_2' not in proto_config: raise ConfigError(f'Redistribute level-1 or level-2 should be specified in ' \ - f'"protocols isis {process} redistribute {afi} {proto}"!') + f'"protocols isis redistribute {afi} {proto}"!') for redistr_level, redistr_config in proto_config.items(): if proc_level and proc_level != 'level_1_2' and proc_level != redistr_level: - raise ConfigError(f'"protocols isis {process} redistribute {afi} {proto} {redistr_level}" ' \ - f'can not be used with \"protocols isis {process} level {proc_level}\"') + raise ConfigError(f'"protocols isis redistribute {afi} {proto} {redistr_level}" ' \ + f'can not be used with \"protocols isis level {proc_level}\"!') # Segment routing checks if dict_search('segment_routing.global_block', isis): @@ -220,8 +220,8 @@ def verify(isis): if ("explicit_null" in prefix_config['index']) and ("no_php_flag" in prefix_config['index']): raise ConfigError(f'Segment routing prefix {prefix} cannot have both explicit-null '\ f'and no-php-flag configured at the same time.') - - # Check for index ranges being larger than the segment routing global block + + # Check for index ranges being larger than the segment routing global block if dict_search('segment_routing.global_block', isis): g_high_label_value = dict_search('segment_routing.global_block.high_label_value', isis) g_low_label_value = dict_search('segment_routing.global_block.low_label_value', isis) @@ -233,7 +233,7 @@ def verify(isis): if int(index_size) > int(g_label_difference): raise ConfigError(f'Segment routing prefix {prefix} cannot have an '\ f'index base size larger than the SRGB label base.') - + # Check for LFA tiebreaker index duplication if dict_search('fast_reroute.lfa.local.tiebreaker', isis): comparison_dictionary = {} @@ -311,4 +311,4 @@ if __name__ == '__main__': apply(c) except ConfigError as e: print(e) - exit(1)
\ No newline at end of file + exit(1) diff --git a/src/conf_mode/protocols_ospf.py b/src/conf_mode/protocols_ospf.py index 695842795..6fffe7e0d 100755 --- a/src/conf_mode/protocols_ospf.py +++ b/src/conf_mode/protocols_ospf.py @@ -127,6 +127,7 @@ def verify(ospf): # Validate if configured Access-list exists if 'area' in ospf: + networks = [] for area, area_config in ospf['area'].items(): if 'import_list' in area_config: acl_import = area_config['import_list'] @@ -135,6 +136,12 @@ def verify(ospf): acl_export = area_config['export_list'] if acl_export: verify_access_list(acl_export, ospf) + if 'network' in area_config: + for network in area_config['network']: + if network in networks: + raise ConfigError(f'Network "{network}" already defined in different area!') + networks.append(network) + if 'interface' in ospf: for interface, interface_config in ospf['interface'].items(): verify_interface_exists(interface) diff --git a/src/conf_mode/service_dhcp-server.py b/src/conf_mode/service_dhcp-server.py index 91ea354b6..ba3d69b07 100755 --- a/src/conf_mode/service_dhcp-server.py +++ b/src/conf_mode/service_dhcp-server.py @@ -143,7 +143,7 @@ def get_config(config=None): dhcp['shared_network_name'][network]['subnet'][subnet].update( {'range' : new_range_dict}) - if dict_search('failover.certificate', dhcp): + if dict_search('high_availability.certificate', dhcp): dhcp['pki'] = conf.get_config_dict(['pki'], key_mangling=('-', '_'), get_first_key=True, no_tag_node_value_mangle=True) return dhcp @@ -286,34 +286,34 @@ def verify(dhcp): if (shared_networks - disabled_shared_networks) < 1: raise ConfigError(f'At least one shared network must be active!') - if 'failover' in dhcp: + if 'high_availability' in dhcp: for key in ['name', 'remote', 'source_address', 'status']: - if key not in dhcp['failover']: + if key not in dhcp['high_availability']: tmp = key.replace('_', '-') - raise ConfigError(f'DHCP failover requires "{tmp}" to be specified!') + raise ConfigError(f'DHCP high-availability requires "{tmp}" to be specified!') - if len({'certificate', 'ca_certificate'} & set(dhcp['failover'])) == 1: - raise ConfigError(f'DHCP secured failover requires both certificate and CA certificate') + if len({'certificate', 'ca_certificate'} & set(dhcp['high_availability'])) == 1: + raise ConfigError(f'DHCP secured high-availability requires both certificate and CA certificate') - if 'certificate' in dhcp['failover']: - cert_name = dhcp['failover']['certificate'] + if 'certificate' in dhcp['high_availability']: + cert_name = dhcp['high_availability']['certificate'] if cert_name not in dhcp['pki']['certificate']: - raise ConfigError(f'Invalid certificate specified for DHCP failover') + raise ConfigError(f'Invalid certificate specified for DHCP high-availability') if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'certificate'): - raise ConfigError(f'Invalid certificate specified for DHCP failover') + raise ConfigError(f'Invalid certificate specified for DHCP high-availability') if not dict_search_args(dhcp['pki']['certificate'], cert_name, 'private', 'key'): - raise ConfigError(f'Missing private key on certificate specified for DHCP failover') + raise ConfigError(f'Missing private key on certificate specified for DHCP high-availability') - if 'ca_certificate' in dhcp['failover']: - ca_cert_name = dhcp['failover']['ca_certificate'] + if 'ca_certificate' in dhcp['high_availability']: + ca_cert_name = dhcp['high_availability']['ca_certificate'] if ca_cert_name not in dhcp['pki']['ca']: - raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability') if not dict_search_args(dhcp['pki']['ca'], ca_cert_name, 'certificate'): - raise ConfigError(f'Invalid CA certificate specified for DHCP failover') + raise ConfigError(f'Invalid CA certificate specified for DHCP high-availability') for address in (dict_search('listen_address', dhcp) or []): if is_addr_assigned(address): @@ -359,23 +359,23 @@ def generate(dhcp): if os.path.exists(f): os.unlink(f) - if 'failover' in dhcp: - if 'certificate' in dhcp['failover']: - cert_name = dhcp['failover']['certificate'] + if 'high_availability' in dhcp: + if 'certificate' in dhcp['high_availability']: + cert_name = dhcp['high_availability']['certificate'] cert_data = dhcp['pki']['certificate'][cert_name]['certificate'] key_data = dhcp['pki']['certificate'][cert_name]['private']['key'] write_file(cert_file, wrap_certificate(cert_data), user=user_group, mode=0o600) write_file(cert_key_file, wrap_private_key(key_data), user=user_group, mode=0o600) - dhcp['failover']['cert_file'] = cert_file - dhcp['failover']['cert_key_file'] = cert_key_file + dhcp['high_availability']['cert_file'] = cert_file + dhcp['high_availability']['cert_key_file'] = cert_key_file - if 'ca_certificate' in dhcp['failover']: - ca_cert_name = dhcp['failover']['ca_certificate'] + if 'ca_certificate' in dhcp['high_availability']: + ca_cert_name = dhcp['high_availability']['ca_certificate'] ca_cert_data = dhcp['pki']['ca'][ca_cert_name]['certificate'] write_file(ca_cert_file, wrap_certificate(ca_cert_data), user=user_group, mode=0o600) - dhcp['failover']['ca_cert_file'] = ca_cert_file + dhcp['high_availability']['ca_cert_file'] = ca_cert_file render(systemd_override, 'dhcp-server/10-override.conf.j2', dhcp) @@ -402,7 +402,7 @@ def apply(dhcp): if service == 'kea-dhcp-ddns-server' and 'dynamic_dns_update' not in dhcp: action = 'stop' - if service == 'kea-ctrl-agent' and 'failover' not in dhcp: + if service == 'kea-ctrl-agent' and 'high_availability' not in dhcp: action = 'stop' call(f'systemctl {action} {service}.service') diff --git a/src/conf_mode/service_router-advert.py b/src/conf_mode/service_router-advert.py index dbb47de4e..88d767bb8 100755 --- a/src/conf_mode/service_router-advert.py +++ b/src/conf_mode/service_router-advert.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2022 VyOS maintainers and contributors +# Copyright (C) 2018-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -17,6 +17,8 @@ import os from sys import exit +from ipaddress import IPv6Network + from vyos.base import Warning from vyos.config import Config from vyos.template import render @@ -47,7 +49,9 @@ def verify(rtradv): return None for interface, interface_config in rtradv['interface'].items(): - if 'prefix' in interface: + interval_max = int(interface_config['interval']['max']) + + if 'prefix' in interface_config: for prefix, prefix_config in interface_config['prefix'].items(): valid_lifetime = prefix_config['valid_lifetime'] if valid_lifetime == 'infinity': @@ -60,6 +64,15 @@ def verify(rtradv): if not (int(valid_lifetime) >= int(preferred_lifetime)): raise ConfigError('Prefix valid-lifetime must be greater then or equal to preferred-lifetime') + if 'nat64prefix' in interface_config: + nat64_supported_lengths = [32, 40, 48, 56, 64, 96] + for prefix, prefix_config in interface_config['nat64prefix'].items(): + if IPv6Network(prefix).prefixlen not in nat64_supported_lengths: + raise ConfigError(f'Invalid NAT64 prefix length for "{prefix}", can only be one of: /' + ', /'.join(nat64_supported_lengths)) + + if int(prefix_config['valid_lifetime']) < interval_max: + raise ConfigError(f'NAT64 valid-lifetime must not be smaller then "interval max" which is "{interval_max}"!') + if 'name_server' in interface_config: if len(interface_config['name_server']) > 3: raise ConfigError('No more then 3 IPv6 name-servers supported!') @@ -72,7 +85,6 @@ def verify(rtradv): # ensure stale RDNSS info gets removed in a timely fashion, this # should not be greater than 2*MaxRtrAdvInterval. lifetime = int(interface_config['name_server_lifetime']) - interval_max = int(interface_config['interval']['max']) if lifetime > 0: if lifetime < int(interval_max): raise ConfigError(f'RDNSS lifetime must be at least "{interval_max}" seconds!') diff --git a/src/conf_mode/system_conntrack.py b/src/conf_mode/system_conntrack.py index e075bc928..3d42389f6 100755 --- a/src/conf_mode/system_conntrack.py +++ b/src/conf_mode/system_conntrack.py @@ -42,33 +42,38 @@ nftables_ct_file = r'/run/nftables-ct.conf' module_map = { 'ftp': { 'ko': ['nf_nat_ftp', 'nf_conntrack_ftp'], - 'nftables': ['ct helper set "ftp_tcp" tcp dport {21} return'] + 'nftables': ['tcp dport {21} ct helper set "ftp_tcp" return'] }, 'h323': { 'ko': ['nf_nat_h323', 'nf_conntrack_h323'], - 'nftables': ['ct helper set "ras_udp" udp dport {1719} return', - 'ct helper set "q931_tcp" tcp dport {1720} return'] + 'nftables': ['udp dport {1719} ct helper set "ras_udp" return', + 'tcp dport {1720} ct helper set "q931_tcp" return'] }, 'nfs': { - 'nftables': ['ct helper set "rpc_tcp" tcp dport {111} return', - 'ct helper set "rpc_udp" udp dport {111} return'] + 'nftables': ['tcp dport {111} ct helper set "rpc_tcp" return', + 'udp dport {111} ct helper set "rpc_udp" return'] }, 'pptp': { 'ko': ['nf_nat_pptp', 'nf_conntrack_pptp'], - 'nftables': ['ct helper set "pptp_tcp" tcp dport {1723} return'], + 'nftables': ['tcp dport {1723} ct helper set "pptp_tcp" return'], 'ipv4': True }, + 'rtsp': { + 'ko': ['nf_nat_rtsp', 'nf_conntrack_rtsp'], + 'nftables': ['tcp dport {554} ct helper set "rtsp_tcp" return'], + 'ipv4': True + }, 'sip': { 'ko': ['nf_nat_sip', 'nf_conntrack_sip'], - 'nftables': ['ct helper set "sip_tcp" tcp dport {5060,5061} return', - 'ct helper set "sip_udp" udp dport {5060,5061} return'] + 'nftables': ['tcp dport {5060,5061} ct helper set "sip_tcp" return', + 'udp dport {5060,5061} ct helper set "sip_udp" return'] }, 'sqlnet': { - 'nftables': ['ct helper set "tns_tcp" tcp dport {1521,1525,1536} return'] + 'nftables': ['tcp dport {1521,1525,1536} ct helper set "tns_tcp" return'] }, 'tftp': { 'ko': ['nf_nat_tftp', 'nf_conntrack_tftp'], - 'nftables': ['ct helper set "tftp_udp" udp dport {69} return'] + 'nftables': ['udp dport {69} ct helper set "tftp_udp" return'] }, } @@ -180,12 +185,16 @@ def generate(conntrack): conntrack['ipv4_firewall_action'] = 'return' conntrack['ipv6_firewall_action'] = 'return' - for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'): - if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()): - if path[0] == 'ipv4': - conntrack['ipv4_firewall_action'] = 'accept' - elif path[0] == 'ipv6': - conntrack['ipv6_firewall_action'] = 'accept' + if dict_search_args(conntrack['firewall'], 'global_options', 'state_policy') != None: + conntrack['ipv4_firewall_action'] = 'accept' + conntrack['ipv6_firewall_action'] = 'accept' + else: + for rules, path in dict_search_recursive(conntrack['firewall'], 'rule'): + if any(('state' in rule_conf or 'connection_status' in rule_conf or 'offload_target' in rule_conf) for rule_conf in rules.values()): + if path[0] == 'ipv4': + conntrack['ipv4_firewall_action'] = 'accept' + elif path[0] == 'ipv6': + conntrack['ipv6_firewall_action'] = 'accept' render(conntrack_config, 'conntrack/vyos_nf_conntrack.conf.j2', conntrack) render(sysctl_file, 'conntrack/sysctl.conf.j2', conntrack) @@ -195,7 +204,7 @@ def generate(conntrack): def apply(conntrack): # Depending on the enable/disable state of the ALG (Application Layer Gateway) # modules we need to either insmod or rmmod the helpers. - + add_modules = [] rm_modules = [] diff --git a/src/etc/ipsec.d/vti-up-down b/src/etc/ipsec.d/vti-up-down index 441b316c2..01e9543c9 100755 --- a/src/etc/ipsec.d/vti-up-down +++ b/src/etc/ipsec.d/vti-up-down @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -57,7 +57,9 @@ if __name__ == '__main__': if 'disable' not in vti: tmp = VTIIf(interface) tmp.update(vti) + call(f'sudo ip link set {interface} up') else: + call(f'sudo ip link set {interface} down') syslog(f'Interface {interface} is admin down ...') elif verb in ['down-client', 'down-host']: if vti_link_up: diff --git a/src/helpers/priority.py b/src/helpers/priority.py new file mode 100755 index 000000000..04186104c --- /dev/null +++ b/src/helpers/priority.py @@ -0,0 +1,42 @@ +#!/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 sys +from argparse import ArgumentParser +from tabulate import tabulate + +from vyos.priority import get_priority_data + +if __name__ == '__main__': + parser = ArgumentParser() + parser.add_argument('--legacy-format', action='store_true', + help="format output for comparison with legacy 'priority.pl'") + args = parser.parse_args() + + prio_list = get_priority_data() + if args.legacy_format: + for p in prio_list: + print(f'{p[2]} {"/".join(p[0])}') + sys.exit(0) + + l = [] + for p in prio_list: + l.append((p[2], p[1], p[0])) + headers = ['priority', 'owner', 'path'] + out = tabulate(l, headers, numalign='right') + print(out) diff --git a/src/helpers/vyos_config_sync.py b/src/helpers/vyos_config_sync.py index 7cfa8fe88..77f7cd810 100755 --- a/src/helpers/vyos_config_sync.py +++ b/src/helpers/vyos_config_sync.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -60,19 +60,20 @@ def post_request(url: str, return response -def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: + +def retrieve_config(section: Optional[List[str]] = None) -> Optional[Dict[str, Any]]: """Retrieves the configuration from the local server. Args: - section: str: The section of the configuration to retrieve. Default is None. + section: List[str]: The section of the configuration to retrieve. + Default is None. Returns: - Optional[Dict[str, Any]]: The retrieved configuration as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The retrieved configuration as a + dictionary, or None if an error occurred. """ if section is None: section = [] - else: - section = section.split() conf = Config() config = conf.get_config_dict(section, get_first_key=True) @@ -84,25 +85,21 @@ def retrieve_config(section: str = None) -> Optional[Dict[str, Any]]: def set_remote_config( address: str, key: str, - op: str, - path: str = None, - section: Optional[str] = None) -> Optional[Dict[str, Any]]: + commands: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: """Loads the VyOS configuration in JSON format to a remote host. Args: address (str): The address of the remote host. key (str): The key to use for loading the configuration. - path (Optional[str]): The path of the configuration. Default is None. - section (Optional[str]): The section of the configuration to load. Default is None. + commands (list): List of set/load commands for request, given as: + [{'op': str, 'path': list[str], 'section': dict}, + ...] Returns: - Optional[Dict[str, Any]]: The response from the remote host as a dictionary, or None if an error occurred. + Optional[Dict[str, Any]]: The response from the remote host as a + dictionary, or None if a RequestException occurred. """ - if path is None: - path = [] - else: - path = path.split() headers = {'Content-Type': 'application/json'} # Disable the InsecureRequestWarning @@ -110,9 +107,7 @@ def set_remote_config( url = f'https://{address}/configure-section' data = json.dumps({ - 'op': mode, - 'path': path, - 'section': section, + 'commands': commands, 'key': key }) @@ -125,19 +120,18 @@ def set_remote_config( return None -def is_section_revised(section: str) -> bool: +def is_section_revised(section: List[str]) -> bool: from vyos.config_mgmt import is_node_revised - return is_node_revised([section]) + return is_node_revised(section) def config_sync(secondary_address: str, secondary_key: str, - sections: List[str], + sections: List[list[str]], mode: str): """Retrieve a config section from primary router in JSON format and send it to secondary router """ - # Config sync only if sections changed if not any(map(is_section_revised, sections)): return @@ -146,21 +140,25 @@ def config_sync(secondary_address: str, ) # Sync sections ("nat", "firewall", etc) + commands = [] for section in sections: config_json = retrieve_config(section=section) # Check if config path deesn't exist, for example "set nat" # we set empty value for config_json data # As we cannot send to the remote host section "nat None" config if not config_json: - config_json = "" + config_json = {} logger.debug( f"Retrieved config for section '{section}': {config_json}") - set_config = set_remote_config(address=secondary_address, - key=secondary_key, - op=mode, - path=section, - section=config_json) - logger.debug(f"Set config for section '{section}': {set_config}") + + d = {'op': mode, 'path': section, 'section': config_json} + commands.append(d) + + set_config = set_remote_config(address=secondary_address, + key=secondary_key, + commands=commands) + + logger.debug(f"Set config for sections '{sections}': {set_config}") if __name__ == '__main__': @@ -188,5 +186,17 @@ if __name__ == '__main__': "Missing required configuration data for config synchronization.") exit(0) + # Generate list_sections of sections/subsections + # [ + # ['interfaces', 'pseudo-ethernet'], ['interfaces', 'virtual-ethernet'], ['nat'], ['nat66'] + # ] + list_sections = [] + for section, subsections in sections.items(): + if subsections: + for subsection in subsections: + list_sections.append([section, subsection]) + else: + list_sections.append([section]) + config_sync(secondary_address, secondary_key, - sections, mode) + list_sections, mode) diff --git a/src/migration-scripts/dhcp-server/10-to-11 b/src/migration-scripts/dhcp-server/10-to-11 new file mode 100755 index 000000000..a0dc96ad0 --- /dev/null +++ b/src/migration-scripts/dhcp-server/10-to-11 @@ -0,0 +1,48 @@ +#!/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/>. + +# T6171: rename "service dhcp-server failover" to "service dhcp-server high-availability" + +from sys import argv +from sys import exit + +from vyos.configtree import ConfigTree + +if len(argv) < 2: + print("Must specify file name!") + exit(1) + +file_name = argv[1] + +with open(file_name, 'r') as f: + config_file = f.read() + +base = ['service', 'dhcp-server'] +config = ConfigTree(config_file) + +if not config.exists(base): + # Nothing to do + exit(0) + +if config.exists(base + ['failover']): + config.rename(base + ['failover'],'high-availability') + +try: + with open(file_name, 'w') as f: + f.write(config.to_string()) +except OSError as e: + print(f'Failed to save the modified config: {e}') + exit(1)
\ No newline at end of file diff --git a/src/migration-scripts/dhcpv6-server/4-to-5 b/src/migration-scripts/dhcpv6-server/4-to-5 index ae506b9c5..55fda91b3 100755 --- a/src/migration-scripts/dhcpv6-server/4-to-5 +++ b/src/migration-scripts/dhcpv6-server/4-to-5 @@ -42,8 +42,11 @@ def find_subnet_interface(subnet): def check_addr(if_path): if config.exists(if_path + ['address']): for addr in config.return_values(if_path + ['address']): - if ip_network(addr, strict=False) == subnet_net: - return True + try: + if ip_network(addr, strict=False) == subnet_net: + return True + except: + pass # interface address was probably "dhcp" or other magic string return None for iftype in config.list_nodes(['interfaces']): diff --git a/src/migration-scripts/policy/1-to-2 b/src/migration-scripts/policy/1-to-2 index c70490ce9..c7a983bba 100755 --- a/src/migration-scripts/policy/1-to-2 +++ b/src/migration-scripts/policy/1-to-2 @@ -32,23 +32,23 @@ file_name = argv[1] with open(file_name, 'r') as f: config_file = f.read() -base = ['policy', 'ipv6-route'] +base = ['policy'] config = ConfigTree(config_file) if not config.exists(base): # Nothing to do exit(0) -config.rename(base, 'route6') -config.set_tag(['policy', 'route6']) +if config.exists(base + ['ipv6-route']): + config.rename(base + ['ipv6-route'],'route6') + config.set_tag(['policy', 'route6']) for route in ['route', 'route6']: - route_path = ['policy', route] - if config.exists(route_path): - for name in config.list_nodes(route_path): - if config.exists(route_path + [name, 'rule']): - for rule in config.list_nodes(route_path + [name, 'rule']): - rule_tcp_flags = route_path + [name, 'rule', rule, 'tcp', 'flags'] + if config.exists(base + [route]): + for name in config.list_nodes(base + [route]): + if config.exists(base + [route, name, 'rule']): + for rule in config.list_nodes(base + [route, name, 'rule']): + rule_tcp_flags = base + [route, name, 'rule', rule, 'tcp', 'flags'] if config.exists(rule_tcp_flags): tmp = config.return_value(rule_tcp_flags) diff --git a/src/migration-scripts/policy/3-to-4 b/src/migration-scripts/policy/3-to-4 index 1ebb248b0..476fa3af2 100755 --- a/src/migration-scripts/policy/3-to-4 +++ b/src/migration-scripts/policy/3-to-4 @@ -51,7 +51,7 @@ def community_migrate(config: ConfigTree, rule: list[str]) -> bool: :rtype: bool """ community_list = list((config.return_value(rule)).split(" ")) - + config.delete(rule) if 'none' in community_list: config.set(rule + ['none']) return False @@ -61,10 +61,8 @@ def community_migrate(config: ConfigTree, rule: list[str]) -> bool: community_action = 'add' community_list.remove('additive') for community in community_list: - if len(community): - config.set(rule + [community_action], value=community, - replace=False) - config.delete(rule) + config.set(rule + [community_action], value=community, + replace=False) if community_action == 'replace': return False else: diff --git a/src/op_mode/conntrack.py b/src/op_mode/conntrack.py index cf8adf795..6ea213bec 100755 --- a/src/op_mode/conntrack.py +++ b/src/op_mode/conntrack.py @@ -112,7 +112,8 @@ def get_formatted_output(dict_data): proto = meta['layer4']['protoname'] if direction == 'independent': conn_id = meta['id'] - timeout = meta['timeout'] + # T6138 flowtable offload conntrack entries without 'timeout' + timeout = meta.get('timeout', 'n/a') orig_src = f'{orig_src}:{orig_sport}' if orig_sport else orig_src orig_dst = f'{orig_dst}:{orig_dport}' if orig_dport else orig_dst reply_src = f'{reply_src}:{reply_sport}' if reply_sport else reply_src diff --git a/src/op_mode/dhcp.py b/src/op_mode/dhcp.py index d27e1baf7..a2f947400 100755 --- a/src/op_mode/dhcp.py +++ b/src/op_mode/dhcp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2023 VyOS maintainers and contributors +# Copyright (C) 2022-2024 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -33,6 +33,7 @@ from vyos.kea import kea_get_leases from vyos.kea import kea_get_pool_from_subnet_id from vyos.kea import kea_delete_lease from vyos.utils.process import is_systemd_service_running +from vyos.utils.process import call time_string = "%a %b %d %H:%M:%S %Z %Y" @@ -309,6 +310,25 @@ def _verify(func): return func(*args, **kwargs) return _wrapper +def _verify_client(func): + """Decorator checks if interface is configured as DHCP client""" + from functools import wraps + from vyos.ifconfig import Section + + @wraps(func) + def _wrapper(*args, **kwargs): + config = ConfigTreeQuery() + family = kwargs.get('family') + v = 'v6' if family == 'inet6' else '' + interface = kwargs.get('interface') + interface_path = Section.get_config_path(interface) + unconf_message = f'DHCP{v} client not configured on interface {interface}!' + + # Check if config does not exist + if not config.exists(f'interfaces {interface_path} address dhcp{v}'): + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper @_verify def show_pool_statistics(raw: bool, family: ArgFamily, pool: typing.Optional[str]): @@ -474,6 +494,16 @@ def show_client_leases(raw: bool, family: ArgFamily, interface: typing.Optional[ else: return _get_formatted_client_leases(lease_data, family=family) +@_verify_client +def renew_client_lease(raw: bool, family: ArgFamily, interface: str): + if not raw: + v = 'v6' if family == 'inet6' else '' + print(f'Restarting DHCP{v} client on interface {interface}...') + if family == 'inet6': + call(f'systemctl restart dhcp6c@{interface}.service') + else: + call(f'systemctl restart dhclient@{interface}.service') + if __name__ == '__main__': try: res = vyos.opmode.run(sys.modules[__name__]) diff --git a/src/op_mode/image_installer.py b/src/op_mode/image_installer.py index 85ebd19ba..b0567305a 100755 --- a/src/op_mode/image_installer.py +++ b/src/op_mode/image_installer.py @@ -786,6 +786,10 @@ def install_image() -> None: grub.install(install_target.name, f'{DIR_DST_ROOT}/boot/', f'{DIR_DST_ROOT}/boot/efi') + # sort inodes (to make GRUB read config files in alphabetical order) + grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS}') + grub.sort_inodes(f'{DIR_DST_ROOT}/{grub.GRUB_DIR_VYOS_VERS}') + # umount filesystems and remove temporary files if is_raid_install(install_target): cleanup([install_target.name], diff --git a/src/services/vyos-http-api-server b/src/services/vyos-http-api-server index a7b14a1a3..77870a84c 100755 --- a/src/services/vyos-http-api-server +++ b/src/services/vyos-http-api-server @@ -463,7 +463,7 @@ def _configure_op(data: Union[ConfigureModel, ConfigureListModel, endpoint = request.url.path # Allow users to pass just one command - if not isinstance(data, ConfigureListModel): + if not isinstance(data, (ConfigureListModel, ConfigSectionListModel)): data = [data] else: data = data.commands diff --git a/src/system/grub_update.py b/src/system/grub_update.py index 5a7d8eb72..5a0534195 100644 --- a/src/system/grub_update.py +++ b/src/system/grub_update.py @@ -105,4 +105,8 @@ if __name__ == '__main__': else: render(grub_cfg_main, grub.TMPL_GRUB_MAIN, {}) + # sort inodes (to make GRUB read config files in alphabetical order) + grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS}') + grub.sort_inodes(f'{root_dir}/{grub.GRUB_DIR_VYOS_VERS}') + exit(0) diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 099f7ed52..d430d8868 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -3,6 +3,7 @@ Description=DHCP client on %i Documentation=man:dhclient(8) StartLimitIntervalSec=0 After=vyos-router.service +ConditionPathExists=/run/dhclient/dhclient_%i.conf [Service] Type=exec diff --git a/src/systemd/vyos-grub-update.service b/src/systemd/vyos-grub-update.service index 522b13a33..7b67ae1b8 100644 --- a/src/systemd/vyos-grub-update.service +++ b/src/systemd/vyos-grub-update.service @@ -6,9 +6,9 @@ Before=vyos-router.service [Service] Type=oneshot ExecStart=/usr/libexec/vyos/system/grub_update.py -TimeoutSec=5 +TimeoutSec=60 KillMode=process StandardOutput=journal+console [Install] -WantedBy=vyos-router.service
\ No newline at end of file +WantedBy=vyos-router.service |