diff options
85 files changed, 1807 insertions, 429 deletions
diff --git a/.github/workflows/package-smoketest.yml b/.github/workflows/package-smoketest.yml index ae34ea2f0..2c90fed39 100644 --- a/.github/workflows/package-smoketest.yml +++ b/.github/workflows/package-smoketest.yml @@ -24,6 +24,7 @@ jobs: build_iso: runs-on: ubuntu-24.04 timeout-minutes: 45 + if: github.repository == 'vyos/vyos-1x' container: image: vyos/vyos-build:current options: --sysctl net.ipv6.conf.lo.disable_ipv6=0 --privileged @@ -63,6 +64,7 @@ jobs: generic - uses: actions/upload-artifact@v4 with: + retention-days: 2 name: vyos-${{ steps.version.outputs.build_version }} path: | build/live-image-amd64.hybrid.iso diff --git a/data/config-mode-dependencies/vyos-1x.json b/data/config-mode-dependencies/vyos-1x.json index cbd14f7c6..7506a0908 100644 --- a/data/config-mode-dependencies/vyos-1x.json +++ b/data/config-mode-dependencies/vyos-1x.json @@ -14,6 +14,9 @@ "vxlan": ["interfaces_vxlan"], "wlan": ["interfaces_wireless"] }, + "interfaces_wireguard": { + "vxlan": ["interfaces_vxlan"] + }, "load_balancing_wan": { "conntrack": ["system_conntrack"] }, diff --git a/data/op-mode-standardized.json b/data/op-mode-standardized.json index 170f0d259..c2bfc3094 100644 --- a/data/op-mode-standardized.json +++ b/data/op-mode-standardized.json @@ -13,6 +13,7 @@ "evpn.py", "interfaces.py", "ipsec.py", +"load-balancing_wan.py", "lldp.py", "log.py", "memory.py", diff --git a/data/templates/accel-ppp/chap-secrets.ipoe.j2 b/data/templates/accel-ppp/chap-secrets.ipoe.j2 index dd85160c0..59b9dfc8d 100644 --- a/data/templates/accel-ppp/chap-secrets.ipoe.j2 +++ b/data/templates/accel-ppp/chap-secrets.ipoe.j2 @@ -6,7 +6,7 @@ {% if mac_config.vlan is vyos_defined %} {% set iface = iface ~ '.' ~ mac_config.vlan %} {% endif %} -{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.static_ip if mac_config.static_ip is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }} +{{ "%-11s" | format(iface) }} * {{ mac | lower }} {{ mac_config.ip_address if mac_config.ip_address is vyos_defined else '*' }} {{ mac_config.rate_limit.download ~ '/' ~ mac_config.rate_limit.upload if mac_config.rate_limit.download is vyos_defined and mac_config.rate_limit.upload is vyos_defined }} {% endfor %} {% endif %} {% endfor %} diff --git a/data/templates/firewall/nftables.j2 b/data/templates/firewall/nftables.j2 index a35143870..67473da8e 100755 --- a/data/templates/firewall/nftables.j2 +++ b/data/templates/firewall/nftables.j2 @@ -435,13 +435,13 @@ table bridge vyos_filter { {% if global_options.state_policy is vyos_defined %} chain VYOS_STATE_POLICY { {% if global_options.state_policy.established is vyos_defined %} - {{ global_options.state_policy.established | nft_state_policy('established') }} + {{ global_options.state_policy.established | nft_state_policy('established', bridge=True) }} {% endif %} {% if global_options.state_policy.invalid is vyos_defined %} - {{ global_options.state_policy.invalid | nft_state_policy('invalid') }} + {{ global_options.state_policy.invalid | nft_state_policy('invalid', bridge=True) }} {% endif %} {% if global_options.state_policy.related is vyos_defined %} - {{ global_options.state_policy.related | nft_state_policy('related') }} + {{ global_options.state_policy.related | nft_state_policy('related', bridge=True) }} {% endif %} return } diff --git a/data/templates/frr/bgpd.frr.j2 b/data/templates/frr/bgpd.frr.j2 index 2f3719fdf..3b462b4a9 100644 --- a/data/templates/frr/bgpd.frr.j2 +++ b/data/templates/frr/bgpd.frr.j2 @@ -310,8 +310,8 @@ router bgp {{ system_as }} {{ 'vrf ' ~ vrf if vrf is vyos_defined }} {% if afi_config.redistribute is vyos_defined %} {% for protocol, protocol_config in afi_config.redistribute.items() %} {% if protocol == 'table' %} -{% for table in protocol_config %} - redistribute table-direct {{ table }} +{% for table, table_config in protocol_config.items() %} + redistribute table-direct {{ table }} {{ 'metric ' ~ table_config.metric if table_config.metric is vyos_defined }} {{ 'route-map ' ~ table_config.route_map if table_config.route_map is vyos_defined }} {% endfor %} {% else %} {% set redistribution_protocol = protocol %} diff --git a/data/templates/frr/policy.frr.j2 b/data/templates/frr/policy.frr.j2 index ed5876ae9..c28633f6f 100644 --- a/data/templates/frr/policy.frr.j2 +++ b/data/templates/frr/policy.frr.j2 @@ -252,6 +252,9 @@ route-map {{ route_map }} {{ rule_config.action }} {{ rule }} {% if rule_config.match.rpki is vyos_defined %} match rpki {{ rule_config.match.rpki }} {% endif %} +{% if rule_config.match.source_vrf is vyos_defined %} + match source-vrf {{ rule_config.match.source_vrf }} +{% endif %} {% if rule_config.match.tag is vyos_defined %} match tag {{ rule_config.match.tag }} {% endif %} diff --git a/data/templates/frr/rpki.frr.j2 b/data/templates/frr/rpki.frr.j2 index 59d5bf0ac..edf0ccaa2 100644 --- a/data/templates/frr/rpki.frr.j2 +++ b/data/templates/frr/rpki.frr.j2 @@ -5,9 +5,9 @@ rpki {% for peer, peer_config in cache.items() %} {# port is mandatory and preference uses a default value #} {% if peer_config.ssh.username is vyos_defined %} - rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }} preference {{ peer_config.preference }} + rpki cache ssh {{ peer | replace('_', '-') }} {{ peer_config.port }} {{ peer_config.ssh.username }} {{ peer_config.ssh.private_key_file }} {{ peer_config.ssh.public_key_file }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }} {% else %} - rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }} preference {{ peer_config.preference }} + rpki cache tcp {{ peer | replace('_', '-') }} {{ peer_config.port }}{{ ' source ' ~ peer_config.source_address if peer_config.source_address is vyos_defined }} preference {{ peer_config.preference }} {% endif %} {% endfor %} {% endif %} diff --git a/data/templates/lldp/vyos.conf.j2 b/data/templates/lldp/vyos.conf.j2 index 4b4228cea..432a7a8e6 100644 --- a/data/templates/lldp/vyos.conf.j2 +++ b/data/templates/lldp/vyos.conf.j2 @@ -4,7 +4,7 @@ configure system platform VyOS configure system description "VyOS {{ version }}" {% if interface is vyos_defined %} {% set tmp = [] %} -{% for iface, iface_options in interface.items() if iface_options.disable is not vyos_defined %} +{% for iface, iface_options in interface.items() %} {% if iface == 'all' %} {% set iface = '*' %} {% endif %} @@ -17,6 +17,15 @@ configure ports {{ iface }} med location elin "{{ iface_options.location.elin }} configure ports {{ iface }} med location coordinate latitude "{{ iface_options.location.coordinate_based.latitude }}" longitude "{{ iface_options.location.coordinate_based.longitude }}" altitude "{{ iface_options.location.coordinate_based.altitude }}m" datum "{{ iface_options.location.coordinate_based.datum }}" {% endif %} {% endif %} +{% set mode = iface_options.mode %} +{% if mode == 'tx' %} +{% set mode = 'tx-only' %} +{% elif mode == 'rx' %} +{% set mode = 'rx-only' %} +{% elif mode == 'rx-tx' %} +{% set mode = 'rx-and-tx' %} +{% endif %} +configure ports {{ iface }} lldp status {{ mode }} {% endfor %} configure system interface pattern "{{ tmp | join(",") }}" {% endif %} diff --git a/data/templates/load-balancing/haproxy.cfg.j2 b/data/templates/load-balancing/haproxy.cfg.j2 index c98b739e2..70ea5d2b0 100644 --- a/data/templates/load-balancing/haproxy.cfg.j2 +++ b/data/templates/load-balancing/haproxy.cfg.j2 @@ -38,9 +38,10 @@ defaults log global mode http option dontlognull - timeout connect 10s - timeout client 50s - timeout server 50s + timeout check {{ timeout.check }}s + timeout connect {{ timeout.connect }}s + timeout client {{ timeout.client }}s + timeout server {{ timeout.server }}s errorfile 400 /etc/haproxy/errors/400.http errorfile 403 /etc/haproxy/errors/403.http errorfile 408 /etc/haproxy/errors/408.http @@ -134,6 +135,11 @@ frontend {{ front }} default_backend {{ backend }} {% endfor %} {% endif %} +{% if front_config.timeout is vyos_defined %} +{% if front_config.timeout.client is vyos_defined %} + timeout client {{ front_config.timeout.client }}s +{% endif %} +{% endif %} {% endfor %} {% endif %} diff --git a/data/templates/load-balancing/nftables-wlb.j2 b/data/templates/load-balancing/nftables-wlb.j2 new file mode 100644 index 000000000..b3d7c3376 --- /dev/null +++ b/data/templates/load-balancing/nftables-wlb.j2 @@ -0,0 +1,64 @@ +#!/usr/sbin/nft -f + +{% if first_install is not vyos_defined %} +delete table ip vyos_wanloadbalance +{% endif %} +table ip vyos_wanloadbalance { + chain wlb_nat_postrouting { + type nat hook postrouting priority srcnat - 1; policy accept; +{% for ifname, health_conf in interface_health.items() if health_state[ifname].if_addr %} +{% if disable_source_nat is not vyos_defined %} +{% set state = health_state[ifname] %} + ct mark {{ state.mark }} counter snat to {{ state.if_addr }} +{% endif %} +{% endfor %} + } + + chain wlb_mangle_prerouting { + type filter hook prerouting priority mangle; policy accept; +{% for ifname, health_conf in interface_health.items() %} +{% set state = health_state[ifname] %} +{% if sticky_connections is vyos_defined %} + iifname "{{ ifname }}" ct state new ct mark set {{ state.mark }} +{% endif %} +{% endfor %} +{% if rule is vyos_defined %} +{% for rule_id, rule_conf in rule.items() %} +{% if rule_conf.exclude is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, exclude=True, action='return') }} +{% else %} +{% set limit = rule_conf.limit is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, limit=limit, weight=True, health_state=health_state) }} + {{ rule_conf | wlb_nft_rule(rule_id, restore_mark=True) }} +{% endif %} +{% endfor %} +{% endif %} + } + + chain wlb_mangle_output { + type filter hook output priority -150; policy accept; +{% if enable_local_traffic is vyos_defined %} + meta mark != 0x0 counter return + meta l4proto icmp counter return + ip saddr 127.0.0.0/8 ip daddr 127.0.0.0/8 counter return +{% if rule is vyos_defined %} +{% for rule_id, rule_conf in rule.items() %} +{% if rule_conf.exclude is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, exclude=True, action='return') }} +{% else %} +{% set limit = rule_conf.limit is vyos_defined %} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, limit=limit, weight=True, health_state=health_state) }} + {{ rule_conf | wlb_nft_rule(rule_id, local=True, restore_mark=True) }} +{% endif %} +{% endfor %} +{% endif %} +{% endif %} + } + +{% for ifname, health_conf in interface_health.items() %} +{% set state = health_state[ifname] %} + chain wlb_mangle_isp_{{ ifname }} { + meta mark set {{ state.mark }} ct mark set {{ state.mark }} counter accept + } +{% endfor %} +} diff --git a/data/templates/load-balancing/wlb.conf.j2 b/data/templates/load-balancing/wlb.conf.j2 deleted file mode 100644 index 7f04d797e..000000000 --- a/data/templates/load-balancing/wlb.conf.j2 +++ /dev/null @@ -1,134 +0,0 @@ -### Autogenerated by load-balancing_wan.py ### - -{% if disable_source_nat is vyos_defined %} -disable-source-nat -{% endif %} -{% if enable_local_traffic is vyos_defined %} -enable-local-traffic -{% endif %} -{% if sticky_connections is vyos_defined %} -sticky-connections inbound -{% endif %} -{% if flush_connections is vyos_defined %} -flush-conntrack -{% endif %} -{% if hook is vyos_defined %} -hook "{{ hook }}" -{% endif %} -{% if interface_health is vyos_defined %} -health { -{% for interface, interface_config in interface_health.items() %} - interface {{ interface }} { -{% if interface_config.failure_count is vyos_defined %} - failure-ct {{ interface_config.failure_count }} -{% endif %} -{% if interface_config.success_count is vyos_defined %} - success-ct {{ interface_config.success_count }} -{% endif %} -{% if interface_config.nexthop is vyos_defined %} - nexthop {{ interface_config.nexthop }} -{% endif %} -{% if interface_config.test is vyos_defined %} -{% for test_rule, test_config in interface_config.test.items() %} - rule {{ test_rule }} { -{% if test_config.type is vyos_defined %} -{% set type_translate = {'ping': 'ping', 'ttl': 'udp', 'user-defined': 'user-defined'} %} - type {{ type_translate[test_config.type] }} { -{% if test_config.ttl_limit is vyos_defined and test_config.type == 'ttl' %} - ttl {{ test_config.ttl_limit }} -{% endif %} -{% if test_config.test_script is vyos_defined and test_config.type == 'user-defined' %} - test-script {{ test_config.test_script }} -{% endif %} -{% if test_config.target is vyos_defined %} - target {{ test_config.target }} -{% endif %} - resp-time {{ test_config.resp_time | int * 1000 }} - } -{% endif %} - } -{% endfor %} -{% endif %} - } -{% endfor %} -} -{% endif %} - -{% if rule is vyos_defined %} -{% for rule, rule_config in rule.items() %} -rule {{ rule }} { -{% if rule_config.exclude is vyos_defined %} - exclude -{% endif %} -{% if rule_config.failover is vyos_defined %} - failover -{% endif %} -{% if rule_config.limit is vyos_defined %} - limit { -{% if rule_config.limit.burst is vyos_defined %} - burst {{ rule_config.limit.burst }} -{% endif %} -{% if rule_config.limit.rate is vyos_defined %} - rate {{ rule_config.limit.rate }} -{% endif %} -{% if rule_config.limit.period is vyos_defined %} - period {{ rule_config.limit.period }} -{% endif %} -{% if rule_config.limit.threshold is vyos_defined %} - thresh {{ rule_config.limit.threshold }} -{% endif %} - } -{% endif %} -{% if rule_config.per_packet_balancing is vyos_defined %} - per-packet-balancing -{% endif %} -{% if rule_config.protocol is vyos_defined %} - protocol {{ rule_config.protocol }} -{% endif %} -{% if rule_config.destination is vyos_defined %} - destination { -{% if rule_config.destination.address is vyos_defined %} - address "{{ rule_config.destination.address }}" -{% endif %} -{% if rule_config.destination.port is vyos_defined %} -{% if '-' in rule_config.destination.port %} - port-ipt "-m multiport --dports {{ rule_config.destination.port | replace('-', ':') }}" -{% elif ',' in rule_config.destination.port %} - port-ipt "-m multiport --dports {{ rule_config.destination.port }}" -{% else %} - port-ipt " --dport {{ rule_config.destination.port }}" -{% endif %} -{% endif %} - } -{% endif %} -{% if rule_config.source is vyos_defined %} - source { -{% if rule_config.source.address is vyos_defined %} - address "{{ rule_config.source.address }}" -{% endif %} -{% if rule_config.source.port is vyos_defined %} -{% if '-' in rule_config.source.port %} - port-ipt "-m multiport --sports {{ rule_config.source.port | replace('-', ':') }}" -{% elif ',' in rule_config.destination.port %} - port-ipt "-m multiport --sports {{ rule_config.source.port }}" -{% else %} - port.ipt " --sport {{ rule_config.source.port }}" -{% endif %} -{% endif %} - } -{% endif %} -{% if rule_config.inbound_interface is vyos_defined %} - inbound-interface {{ rule_config.inbound_interface }} -{% endif %} -{% if rule_config.interface is vyos_defined %} -{% for interface, interface_config in rule_config.interface.items() %} - interface {{ interface }} { -{% if interface_config.weight is vyos_defined %} - weight {{ interface_config.weight }} -{% endif %} - } -{% endfor %} -{% endif %} -} -{% endfor %} -{% endif %} diff --git a/debian/control b/debian/control index 57709ea24..0d040a374 100644 --- a/debian/control +++ b/debian/control @@ -203,9 +203,6 @@ Depends: # For "load-balancing haproxy" haproxy, # End "load-balancing haproxy" -# For "load-balancing wan" - vyatta-wanloadbalance, -# End "load-balancing wan" # For "service dhcp-relay" isc-dhcp-relay, # For "service dhcp-server" diff --git a/interface-definitions/container.xml.in b/interface-definitions/container.xml.in index 04318a7c9..5c320e8c6 100644 --- a/interface-definitions/container.xml.in +++ b/interface-definitions/container.xml.in @@ -31,7 +31,7 @@ <properties> <help>Grant individual Linux capability to container instance</help> <completionHelp> - <list>net-admin net-bind-service net-raw setpcap sys-admin sys-module sys-nice sys-time</list> + <list>net-admin net-bind-service net-raw mknod setpcap sys-admin sys-module sys-nice sys-time</list> </completionHelp> <valueHelp> <format>net-admin</format> @@ -46,6 +46,10 @@ <description>Permission to create raw network sockets</description> </valueHelp> <valueHelp> + <format>mknod</format> + <description>Permission to create special files</description> + </valueHelp> + <valueHelp> <format>setpcap</format> <description>Capability sets (from bounded or inherited set)</description> </valueHelp> @@ -66,7 +70,7 @@ <description>Permission to set system clock</description> </valueHelp> <constraint> - <regex>(net-admin|net-bind-service|net-raw|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex> + <regex>(net-admin|net-bind-service|net-raw|mknod|setpcap|sys-admin|sys-module|sys-nice|sys-time)</regex> </constraint> <multi/> </properties> @@ -412,6 +416,35 @@ </constraint> </properties> </leafNode> + <tagNode name="tmpfs"> + <properties> + <help>Mount a tmpfs filesystem into the container</help> + </properties> + <children> + <leafNode name="destination"> + <properties> + <help>Destination container directory</help> + <valueHelp> + <format>txt</format> + <description>Destination container directory</description> + </valueHelp> + </properties> + </leafNode> + <leafNode name="size"> + <properties> + <help>tmpfs filesystem size in MB</help> + <valueHelp> + <format>u32:1-65536</format> + <description>tmpfs filesystem size in MB</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-65535"/> + </constraint> + <constraintErrorMessage>Container tmpfs size must be between 1 and 65535 MB</constraintErrorMessage> + </properties> + </leafNode> + </children> + </tagNode> <tagNode name="volume"> <properties> <help>Mount a volume into the container</help> diff --git a/interface-definitions/include/babel/redistribute-common.xml.i b/interface-definitions/include/babel/redistribute-common.xml.i index 93efe68dd..e988cc0d0 100644 --- a/interface-definitions/include/babel/redistribute-common.xml.i +++ b/interface-definitions/include/babel/redistribute-common.xml.i @@ -23,6 +23,12 @@ <valueless/> </properties> </leafNode> + <leafNode name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + <valueless/> + </properties> + </leafNode> <leafNode name="openfabric"> <properties> <help>OpenFabric Routing Protocol</help> diff --git a/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i index f4d18bc70..3f6517d03 100644 --- a/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i +++ b/interface-definitions/include/bgp/afi-redistribute-common-protocols.xml.i @@ -31,6 +31,14 @@ #include <include/bgp/afi-redistribute-metric-route-map.xml.i> </children> </node> +<node name="nhrp"> + <properties> + <help>Redistribute NHRP routes into BGP</help> + </properties> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</node> <node name="static"> <properties> <help>Redistribute static routes into BGP</help> @@ -39,14 +47,16 @@ #include <include/bgp/afi-redistribute-metric-route-map.xml.i> </children> </node> -<leafNode name="table"> +<tagNode name="table"> <properties> <help>Redistribute non-main Kernel Routing Table</help> <completionHelp> <path>protocols static table</path> </completionHelp> #include <include/constraint/protocols-static-table.xml.i> - <multi/> </properties> -</leafNode> + <children> + #include <include/bgp/afi-redistribute-metric-route-map.xml.i> + </children> +</tagNode> <!-- include end --> diff --git a/interface-definitions/include/constraint/interface-name.xml.i b/interface-definitions/include/constraint/interface-name.xml.i index 3e7c4e667..bf1db243d 100644 --- a/interface-definitions/include/constraint/interface-name.xml.i +++ b/interface-definitions/include/constraint/interface-name.xml.i @@ -1,4 +1,4 @@ <!-- include start from constraint/interface-name.xml.i --> -<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|lo</regex> +<regex>(bond|br|dum|en|ersp|eth|gnv|ifb|ipoe|lan|l2tp|l2tpeth|macsec|peth|ppp|pppoe|pptp|sstp|sstpc|tun|veth|vti|vtun|vxlan|wg|wlan|wwan)[0-9]+(.\d+)?|pod-[-_a-zA-Z0-9]{1,11}|lo</regex> <validator name="file-path --lookup-path /sys/class/net --directory"/> <!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-check.xml.i b/interface-definitions/include/haproxy/timeout-check.xml.i new file mode 100644 index 000000000..d1217fac3 --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-check.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-check.xml.i --> +<leafNode name="check"> + <properties> + <help>Timeout in seconds for established connections</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Check timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-client.xml.i b/interface-definitions/include/haproxy/timeout-client.xml.i new file mode 100644 index 000000000..2250ccdef --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-client.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-client.xml.i --> +<leafNode name="client"> + <properties> + <help>Maximum inactivity time on the client side</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-connect.xml.i b/interface-definitions/include/haproxy/timeout-connect.xml.i new file mode 100644 index 000000000..da4f983af --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-connect.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-connect.xml.i --> +<leafNode name="connect"> + <properties> + <help>Set the maximum time to wait for a connection attempt to a server to succeed</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Connect timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout-server.xml.i b/interface-definitions/include/haproxy/timeout-server.xml.i new file mode 100644 index 000000000..f27d415c1 --- /dev/null +++ b/interface-definitions/include/haproxy/timeout-server.xml.i @@ -0,0 +1,14 @@ +<!-- include start from haproxy/timeout-server.xml.i --> +<leafNode name="server"> + <properties> + <help>Set the maximum inactivity time on the server side</help> + <valueHelp> + <format>u32:1-3600</format> + <description>Server timeout in seconds</description> + </valueHelp> + <constraint> + <validator name="numeric" argument="--range 1-3600"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/haproxy/timeout.xml.i b/interface-definitions/include/haproxy/timeout.xml.i index 79e7303b1..a3a5a8a3e 100644 --- a/interface-definitions/include/haproxy/timeout.xml.i +++ b/interface-definitions/include/haproxy/timeout.xml.i @@ -4,42 +4,9 @@ <help>Timeout options</help> </properties> <children> - <leafNode name="check"> - <properties> - <help>Timeout in seconds for established connections</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Check timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> - <leafNode name="connect"> - <properties> - <help>Set the maximum time to wait for a connection attempt to a server to succeed</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Connect timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> - <leafNode name="server"> - <properties> - <help>Set the maximum inactivity time on the server side</help> - <valueHelp> - <format>u32:1-3600</format> - <description>Server timeout in seconds</description> - </valueHelp> - <constraint> - <validator name="numeric" argument="--range 1-3600"/> - </constraint> - </properties> - </leafNode> + #include <include/haproxy/timeout-check.xml.i> + #include <include/haproxy/timeout-connect.xml.i> + #include <include/haproxy/timeout-server.xml.i> </children> </node> <!-- include end --> diff --git a/interface-definitions/include/ip-address.xml.i b/interface-definitions/include/ip-address.xml.i new file mode 100644 index 000000000..6027e97ee --- /dev/null +++ b/interface-definitions/include/ip-address.xml.i @@ -0,0 +1,14 @@ +<!-- include start from ip-address.xml.i --> +<leafNode name="ip-address"> + <properties> + <help>Fixed IP address of static mapping</help> + <valueHelp> + <format>ipv4</format> + <description>IPv4 address used in static mapping</description> + </valueHelp> + <constraint> + <validator name="ipv4-address"/> + </constraint> + </properties> +</leafNode> +<!-- include end --> diff --git a/interface-definitions/include/isis/protocol-common-config.xml.i b/interface-definitions/include/isis/protocol-common-config.xml.i index 35ce80be9..e0a7e62b6 100644 --- a/interface-definitions/include/isis/protocol-common-config.xml.i +++ b/interface-definitions/include/isis/protocol-common-config.xml.i @@ -418,6 +418,14 @@ #include <include/isis/redistribute-level-1-2.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes into IS-IS</help> + </properties> + <children> + #include <include/isis/redistribute-level-1-2.xml.i> + </children> + </node> <node name="ospf"> <properties> <help>Redistribute OSPF routes into IS-IS</help> diff --git a/interface-definitions/include/ospf/protocol-common-config.xml.i b/interface-definitions/include/ospf/protocol-common-config.xml.i index cef832381..f597be64e 100644 --- a/interface-definitions/include/ospf/protocol-common-config.xml.i +++ b/interface-definitions/include/ospf/protocol-common-config.xml.i @@ -798,6 +798,16 @@ #include <include/route-map.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + </properties> + <children> + #include <include/ospf/metric.xml.i> + #include <include/ospf/metric-type.xml.i> + #include <include/route-map.xml.i> + </children> + </node> <node name="rip"> <properties> <help>Redistribute RIP routes</help> diff --git a/interface-definitions/include/policy/community-value-list.xml.i b/interface-definitions/include/policy/community-value-list.xml.i index 8c665c5f0..b1499440a 100644 --- a/interface-definitions/include/policy/community-value-list.xml.i +++ b/interface-definitions/include/policy/community-value-list.xml.i @@ -4,7 +4,6 @@ local-as no-advertise no-export - internet graceful-shutdown accept-own route-filter-translated-v4 @@ -35,10 +34,6 @@ <description>Well-known communities value NO_EXPORT 0xFFFFFF01</description> </valueHelp> <valueHelp> - <format>internet</format> - <description>Well-known communities value 0</description> -</valueHelp> -<valueHelp> <format>graceful-shutdown</format> <description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description> </valueHelp> @@ -84,7 +79,7 @@ </valueHelp> <multi/> <constraint> - <regex>local-as|no-advertise|no-export|internet|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex> + <regex>local-as|no-advertise|no-export|graceful-shutdown|accept-own|route-filter-translated-v4|route-filter-v4|route-filter-translated-v6|route-filter-v6|llgr-stale|no-llgr|accept-own-nexthop|blackhole|no-peer</regex> <validator name="bgp-regular-community"/> </constraint> <!-- include end --> diff --git a/interface-definitions/include/version/bgp-version.xml.i b/interface-definitions/include/version/bgp-version.xml.i index 6bed7189f..c90276151 100644 --- a/interface-definitions/include/version/bgp-version.xml.i +++ b/interface-definitions/include/version/bgp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/bgp-version.xml.i --> -<syntaxVersion component='bgp' version='5'></syntaxVersion> +<syntaxVersion component='bgp' version='6'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/lldp-version.xml.i b/interface-definitions/include/version/lldp-version.xml.i index b41d80451..a7110691a 100644 --- a/interface-definitions/include/version/lldp-version.xml.i +++ b/interface-definitions/include/version/lldp-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/lldp-version.xml.i --> -<syntaxVersion component='lldp' version='2'></syntaxVersion> +<syntaxVersion component='lldp' version='3'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/policy-version.xml.i b/interface-definitions/include/version/policy-version.xml.i index db727fea9..5c53a4032 100644 --- a/interface-definitions/include/version/policy-version.xml.i +++ b/interface-definitions/include/version/policy-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/policy-version.xml.i --> -<syntaxVersion component='policy' version='8'></syntaxVersion> +<syntaxVersion component='policy' version='9'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/include/version/wanloadbalance-version.xml.i b/interface-definitions/include/version/wanloadbalance-version.xml.i index 59f8729cc..34c3c76ff 100644 --- a/interface-definitions/include/version/wanloadbalance-version.xml.i +++ b/interface-definitions/include/version/wanloadbalance-version.xml.i @@ -1,3 +1,3 @@ <!-- include start from include/version/wanloadbalance-version.xml.i --> -<syntaxVersion component='wanloadbalance' version='3'></syntaxVersion> +<syntaxVersion component='wanloadbalance' version='4'></syntaxVersion> <!-- include end --> diff --git a/interface-definitions/interfaces_geneve.xml.in b/interface-definitions/interfaces_geneve.xml.in index 990c5bd91..c1e6c33d5 100644 --- a/interface-definitions/interfaces_geneve.xml.in +++ b/interface-definitions/interfaces_geneve.xml.in @@ -23,6 +23,10 @@ #include <include/interface/ipv6-options.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/mtu-1200-16000.xml.i> + #include <include/port-number.xml.i> + <leafNode name="port"> + <defaultValue>6081</defaultValue> + </leafNode> <node name="parameters"> <properties> <help>GENEVE tunnel parameters</help> diff --git a/interface-definitions/load-balancing_haproxy.xml.in b/interface-definitions/load-balancing_haproxy.xml.in index ca089d3f0..b95e02337 100644 --- a/interface-definitions/load-balancing_haproxy.xml.in +++ b/interface-definitions/load-balancing_haproxy.xml.in @@ -48,6 +48,14 @@ <valueless/> </properties> </leafNode> + <node name="timeout"> + <properties> + <help>Timeout options</help> + </properties> + <children> + #include <include/haproxy/timeout-client.xml.i> + </children> + </node> <node name="http-compression"> <properties> <help>Compress HTTP responses</help> @@ -368,6 +376,29 @@ </leafNode> </children> </node> + <node name="timeout"> + <properties> + <help>Timeout options</help> + </properties> + <children> + #include <include/haproxy/timeout-check.xml.i> + <leafNode name="check"> + <defaultValue>5</defaultValue> + </leafNode> + #include <include/haproxy/timeout-connect.xml.i> + <leafNode name="connect"> + <defaultValue>10</defaultValue> + </leafNode> + #include <include/haproxy/timeout-client.xml.i> + <leafNode name="client"> + <defaultValue>50</defaultValue> + </leafNode> + #include <include/haproxy/timeout-server.xml.i> + <leafNode name="server"> + <defaultValue>50</defaultValue> + </leafNode> + </children> + </node> #include <include/interface/vrf.xml.i> </children> </node> diff --git a/interface-definitions/policy.xml.in b/interface-definitions/policy.xml.in index cbab6173f..25dbf5581 100644 --- a/interface-definitions/policy.xml.in +++ b/interface-definitions/policy.xml.in @@ -202,7 +202,7 @@ <properties> <help>Regular expression to match against a community-list</help> <completionHelp> - <list>local-AS no-advertise no-export internet graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list> + <list>local-AS no-advertise no-export graceful-shutdown accept-own-nexthop accept-own route-filter-translated-v4 route-filter-v4 route-filter-translated-v6 route-filter-v6 llgr-stale no-llgr blackhole no-peer additive</list> </completionHelp> <valueHelp> <format><aa:nn></format> @@ -221,10 +221,6 @@ <description>Well-known communities value NO_EXPORT 0xFFFFFF01</description> </valueHelp> <valueHelp> - <format>internet</format> - <description>Well-known communities value 0</description> - </valueHelp> - <valueHelp> <format>graceful-shutdown</format> <description>Well-known communities value GRACEFUL_SHUTDOWN 0xFFFF0000</description> </valueHelp> @@ -1096,6 +1092,20 @@ </constraint> </properties> </leafNode> + <leafNode name="source-vrf"> + <properties> + <help>Source vrf</help> + #include <include/constraint/vrf.xml.i> + <valueHelp> + <format>txt</format> + <description>VRF instance name</description> + </valueHelp> + <completionHelp> + <path>vrf name</path> + <list>default</list> + </completionHelp> + </properties> + </leafNode> #include <include/policy/tag.xml.i> </children> </node> diff --git a/interface-definitions/protocols_rip.xml.in b/interface-definitions/protocols_rip.xml.in index 0edd8f2ce..745280fd7 100644 --- a/interface-definitions/protocols_rip.xml.in +++ b/interface-definitions/protocols_rip.xml.in @@ -209,6 +209,14 @@ #include <include/rip/redistribute.xml.i> </children> </node> + <node name="nhrp"> + <properties> + <help>Redistribute NHRP routes</help> + </properties> + <children> + #include <include/rip/redistribute.xml.i> + </children> + </node> <node name="ospf"> <properties> <help>Redistribute OSPF routes</help> diff --git a/interface-definitions/protocols_rpki.xml.in b/interface-definitions/protocols_rpki.xml.in index 54d69eadb..9e2e84717 100644 --- a/interface-definitions/protocols_rpki.xml.in +++ b/interface-definitions/protocols_rpki.xml.in @@ -42,6 +42,7 @@ </constraint> </properties> </leafNode> + #include <include/source-address-ipv4.xml.i> <node name="ssh"> <properties> <help>RPKI SSH connection settings</help> diff --git a/interface-definitions/service_dhcp-server.xml.in b/interface-definitions/service_dhcp-server.xml.in index cb5f9a804..9a194de4f 100644 --- a/interface-definitions/service_dhcp-server.xml.in +++ b/interface-definitions/service_dhcp-server.xml.in @@ -211,18 +211,7 @@ #include <include/dhcp/option-v4.xml.i> #include <include/generic-description.xml.i> #include <include/generic-disable-node.xml.i> - <leafNode name="ip-address"> - <properties> - <help>Fixed IP address of static mapping</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address used in static mapping</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/ip-address.xml.i> #include <include/interface/mac.xml.i> #include <include/interface/duid.xml.i> </children> diff --git a/interface-definitions/service_ipoe-server.xml.in b/interface-definitions/service_ipoe-server.xml.in index 6cc4471af..fe9d32bbd 100644 --- a/interface-definitions/service_ipoe-server.xml.in +++ b/interface-definitions/service_ipoe-server.xml.in @@ -70,18 +70,7 @@ <constraintErrorMessage>VLAN IDs need to be in range 1-4094</constraintErrorMessage> </properties> </leafNode> - <leafNode name="static-ip"> - <properties> - <help>Static client IP address</help> - <valueHelp> - <format>ipv4</format> - <description>IPv4 address</description> - </valueHelp> - <constraint> - <validator name="ipv4-address"/> - </constraint> - </properties> - </leafNode> + #include <include/ip-address.xml.i> </children> </tagNode> </children> diff --git a/interface-definitions/service_lldp.xml.in b/interface-definitions/service_lldp.xml.in index 51a9f9cce..a189cc13b 100644 --- a/interface-definitions/service_lldp.xml.in +++ b/interface-definitions/service_lldp.xml.in @@ -29,7 +29,34 @@ </constraint> </properties> <children> - #include <include/generic-disable-node.xml.i> + <leafNode name="mode"> + <properties> + <help>Set LLDP receive/transmit operation mode of this interface</help> + <completionHelp> + <list>disable rx-tx tx rx</list> + </completionHelp> + <valueHelp> + <format>disable</format> + <description>Do not process or send LLDP messages</description> + </valueHelp> + <valueHelp> + <format>rx-tx</format> + <description>Send and process LLDP messages</description> + </valueHelp> + <valueHelp> + <format>rx</format> + <description>Process incoming LLDP messages</description> + </valueHelp> + <valueHelp> + <format>tx</format> + <description>Send LLDP messages</description> + </valueHelp> + <constraint> + <regex>(disable|rx-tx|tx|rx)</regex> + </constraint> + </properties> + <defaultValue>rx-tx</defaultValue> + </leafNode> <node name="location"> <properties> <help>LLDP-MED location data</help> diff --git a/interface-definitions/service_snmp.xml.in b/interface-definitions/service_snmp.xml.in index f23151ef9..cc21f5b8b 100644 --- a/interface-definitions/service_snmp.xml.in +++ b/interface-definitions/service_snmp.xml.in @@ -304,7 +304,6 @@ </constraint> <constraintErrorMessage>ID must contain an even number (from 2 to 36) of hex digits</constraintErrorMessage> </properties> - <defaultValue></defaultValue> </leafNode> <tagNode name="group"> <properties> diff --git a/op-mode-definitions/load-balacing_haproxy.in b/op-mode-definitions/load-balancing_haproxy.xml.in index c3d6c799b..8de7ae97f 100644 --- a/op-mode-definitions/load-balacing_haproxy.in +++ b/op-mode-definitions/load-balancing_haproxy.xml.in @@ -16,7 +16,7 @@ <properties> <help>Show load-balancing haproxy</help> </properties> - <command>sudo ${vyos_op_scripts_dir}/load-balacing_haproxy.py show</command> + <command>sudo ${vyos_op_scripts_dir}/load-balancing_haproxy.py show</command> </node> </children> </node> diff --git a/op-mode-definitions/load-balancing_wan.xml.in b/op-mode-definitions/load-balancing_wan.xml.in new file mode 100644 index 000000000..91c57c1f4 --- /dev/null +++ b/op-mode-definitions/load-balancing_wan.xml.in @@ -0,0 +1,37 @@ +<?xml version="1.0" encoding="UTF-8"?> +<interfaceDefinition> + <node name="restart"> + <children> + <node name="wan-load-balance"> + <properties> + <help>Restart Wide Area Network (WAN) load-balancing daemon</help> + </properties> + <command>sudo ${vyos_op_scripts_dir}/restart.py restart_service --name load-balancing_wan</command> + </node> + </children> + </node> + <node name="show"> + <children> + <node name="wan-load-balance"> + <properties> + <help>Show Wide Area Network (WAN) load-balancing information</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_summary</command> + <children> + <node name="connection"> + <properties> + <help>Show Wide Area Network (WAN) load-balancing flow</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_connection</command> + </node> + <node name="status"> + <properties> + <help>Show WAN load-balancing statistics</help> + </properties> + <command>${vyos_op_scripts_dir}/load-balancing_wan.py show_status</command> + </node> + </children> + </node> + </children> + </node> +</interfaceDefinition>
\ No newline at end of file diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 89e51707b..86194cd55 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -37,7 +37,8 @@ directories = { 'dhcp6_client_dir' : '/run/dhcp6c', 'vyos_configdir' : '/opt/vyatta/config', 'completion_dir' : f'{base_dir}/completion', - 'ca_certificates' : '/usr/local/share/ca-certificates/vyos' + 'ca_certificates' : '/usr/local/share/ca-certificates/vyos', + 'ppp_nexthop_dir' : '/run/ppp_nexthop' } systemd_services = { diff --git a/python/vyos/ifconfig/geneve.py b/python/vyos/ifconfig/geneve.py index f7fddb812..f53ef4166 100644 --- a/python/vyos/ifconfig/geneve.py +++ b/python/vyos/ifconfig/geneve.py @@ -48,7 +48,7 @@ class GeneveIf(Interface): 'parameters.ipv6.flowlabel' : 'flowlabel', } - cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote}' + cmd = 'ip link add name {ifname} type geneve id {vni} remote {remote} dstport {port}' for vyos_key, iproute2_key in mapping.items(): # dict_search will return an empty dict "{}" for valueless nodes like # "parameters.nolearning" - thus we need to test the nodes existence diff --git a/python/vyos/ifconfig/wireguard.py b/python/vyos/ifconfig/wireguard.py index fed7a5f84..be9bffd20 100644 --- a/python/vyos/ifconfig/wireguard.py +++ b/python/vyos/ifconfig/wireguard.py @@ -77,6 +77,84 @@ class WireGuardOperational(Operational): } return output + def show_interface(self): + from vyos.config import Config + + c = Config() + + wgdump = self._dump().get(self.config['ifname'], None) + + c.set_level(['interfaces', 'wireguard', self.config['ifname']]) + description = c.return_effective_value(['description']) + ips = c.return_effective_values(['address']) + hostnames = c.return_effective_values(['host-name']) + + answer = 'interface: {}\n'.format(self.config['ifname']) + if description: + answer += ' description: {}\n'.format(description) + if ips: + answer += ' address: {}\n'.format(', '.join(ips)) + if hostnames: + answer += ' hostname: {}\n'.format(', '.join(hostnames)) + + answer += ' public key: {}\n'.format(wgdump['public_key']) + answer += ' private key: (hidden)\n' + answer += ' listening port: {}\n'.format(wgdump['listen_port']) + answer += '\n' + + for peer in c.list_effective_nodes(['peer']): + if wgdump['peers']: + pubkey = c.return_effective_value(['peer', peer, 'public-key']) + if pubkey in wgdump['peers']: + wgpeer = wgdump['peers'][pubkey] + + answer += ' peer: {}\n'.format(peer) + answer += ' public key: {}\n'.format(pubkey) + + """ figure out if the tunnel is recently active or not """ + status = 'inactive' + if wgpeer['latest_handshake'] is None: + """ no handshake ever """ + status = 'inactive' + else: + if int(wgpeer['latest_handshake']) > 0: + delta = timedelta( + seconds=int(time.time() - wgpeer['latest_handshake']) + ) + answer += ' latest handshake: {}\n'.format(delta) + if time.time() - int(wgpeer['latest_handshake']) < (60 * 5): + """ Five minutes and the tunnel is still active """ + status = 'active' + else: + """ it's been longer than 5 minutes """ + status = 'inactive' + elif int(wgpeer['latest_handshake']) == 0: + """ no handshake ever """ + status = 'inactive' + answer += ' status: {}\n'.format(status) + + if wgpeer['endpoint'] is not None: + answer += ' endpoint: {}\n'.format(wgpeer['endpoint']) + + if wgpeer['allowed_ips'] is not None: + answer += ' allowed ips: {}\n'.format( + ','.join(wgpeer['allowed_ips']).replace(',', ', ') + ) + + if wgpeer['transfer_rx'] > 0 or wgpeer['transfer_tx'] > 0: + rx_size = size(wgpeer['transfer_rx'], system=alternative) + tx_size = size(wgpeer['transfer_tx'], system=alternative) + answer += ' transfer: {} received, {} sent\n'.format( + rx_size, tx_size + ) + + if wgpeer['persistent_keepalive'] is not None: + answer += ' persistent keepalive: every {} seconds\n'.format( + wgpeer['persistent_keepalive'] + ) + answer += '\n' + return answer + def get_latest_handshakes(self): """Get latest handshake time for each peer""" output = {} diff --git a/python/vyos/template.py b/python/vyos/template.py index be9f781a6..e75db1a8d 100755 --- a/python/vyos/template.py +++ b/python/vyos/template.py @@ -612,12 +612,17 @@ def nft_default_rule(fw_conf, fw_name, family): return " ".join(output) @register_filter('nft_state_policy') -def nft_state_policy(conf, state): +def nft_state_policy(conf, state, bridge=False): out = [f'ct state {state}'] + action = conf['action'] if 'action' in conf else None + + if bridge and action == 'reject': + action = 'drop' # T7148 - Bridge cannot use reject + if 'log' in conf: log_state = state[:3].upper() - log_action = (conf['action'] if 'action' in conf else 'accept')[:1].upper() + log_action = (action if action else 'accept')[:1].upper() out.append(f'log prefix "[STATE-POLICY-{log_state}-{log_action}]"') if 'log_level' in conf: @@ -626,8 +631,8 @@ def nft_state_policy(conf, state): out.append('counter') - if 'action' in conf: - out.append(conf['action']) + if action: + out.append(action) return " ".join(out) @@ -779,6 +784,11 @@ def conntrack_ct_policy(protocol_conf): return ", ".join(output) +@register_filter('wlb_nft_rule') +def wlb_nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False): + from vyos.wanloadbalance import nft_rule as wlb_nft_rule + return wlb_nft_rule(rule_conf, rule_id, local, exclude, limit, weight, health_state, action, restore_mark) + @register_filter('range_to_regex') def range_to_regex(num_range): """Convert range of numbers or list of ranges diff --git a/python/vyos/wanloadbalance.py b/python/vyos/wanloadbalance.py new file mode 100644 index 000000000..62e109f21 --- /dev/null +++ b/python/vyos/wanloadbalance.py @@ -0,0 +1,153 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import os + +from vyos.defaults import directories +from vyos.utils.process import run + +dhclient_lease = 'dhclient_{0}.lease' + +def nft_rule(rule_conf, rule_id, local=False, exclude=False, limit=False, weight=None, health_state=None, action=None, restore_mark=False): + output = [] + + if 'inbound_interface' in rule_conf: + ifname = rule_conf['inbound_interface'] + if local and not exclude: + output.append(f'oifname != "{ifname}"') + elif not local: + output.append(f'iifname "{ifname}"') + + if 'protocol' in rule_conf and rule_conf['protocol'] != 'all': + protocol = rule_conf['protocol'] + operator = '' + + if protocol[:1] == '!': + operator = '!=' + protocol = protocol[1:] + + if protocol == 'tcp_udp': + protocol = '{ tcp, udp }' + + output.append(f'meta l4proto {operator} {protocol}') + + for direction in ['source', 'destination']: + if direction not in rule_conf: + continue + + direction_conf = rule_conf[direction] + prefix = direction[:1] + + if 'address' in direction_conf: + operator = '' + address = direction_conf['address'] + if address[:1] == '!': + operator = '!=' + address = address[1:] + output.append(f'ip {prefix}addr {operator} {address}') + + if 'port' in direction_conf: + operator = '' + port = direction_conf['port'] + if port[:1] == '!': + operator = '!=' + port = port[1:] + output.append(f'th {prefix}port {operator} {port}') + + if 'source_based_routing' not in rule_conf and not restore_mark: + output.append('ct state new') + + if limit and 'limit' in rule_conf and 'rate' in rule_conf['limit']: + output.append(f'limit rate {rule_conf["limit"]["rate"]}/{rule_conf["limit"]["period"]}') + if 'burst' in rule_conf['limit']: + output.append(f'burst {rule_conf["limit"]["burst"]} packets') + + output.append('counter') + + if restore_mark: + output.append('meta mark set ct mark') + elif weight: + weights, total_weight = wlb_weight_interfaces(rule_conf, health_state) + if len(weights) > 1: # Create weight-based verdict map + vmap_str = ", ".join(f'{weight} : jump wlb_mangle_isp_{ifname}' for ifname, weight in weights) + output.append(f'numgen random mod {total_weight} vmap {{ {vmap_str} }}') + elif len(weights) == 1: # Jump to single ISP + ifname, _ = weights[0] + output.append(f'jump wlb_mangle_isp_{ifname}') + else: # No healthy interfaces + return "" + elif action: + output.append(action) + + return " ".join(output) + +def wlb_weight_interfaces(rule_conf, health_state): + interfaces = [] + + for ifname, if_conf in rule_conf['interface'].items(): + if ifname in health_state and health_state[ifname]['state']: + weight = int(if_conf.get('weight', 1)) + interfaces.append((ifname, weight)) + + if not interfaces: + return [], 0 + + if 'failover' in rule_conf: + for ifpair in sorted(interfaces, key=lambda i: i[1], reverse=True): + return [ifpair], ifpair[1] # Return highest weight interface that is ACTIVE when in failover + + total_weight = sum(weight for _, weight in interfaces) + out = [] + start = 0 + for ifname, weight in sorted(interfaces, key=lambda i: i[1]): # build weight ranges + end = start + weight - 1 + out.append((ifname, f'{start}-{end}' if end > start else start)) + start = weight + + return out, total_weight + +def health_ping_host(host, ifname, count=1, wait_time=0): + cmd_str = f'ping -c {count} -W {wait_time} -I {ifname} {host}' + rc = run(cmd_str) + return rc == 0 + +def health_ping_host_ttl(host, ifname, count=1, ttl_limit=0): + cmd_str = f'ping -c {count} -t {ttl_limit} -I {ifname} {host}' + rc = run(cmd_str) + return rc != 0 + +def parse_dhcp_nexthop(ifname): + lease_file = os.path.join(directories['isc_dhclient_dir'], dhclient_lease.format(ifname)) + + if not os.path.exists(lease_file): + return False + + with open(lease_file, 'r') as f: + for line in f.readlines(): + data = line.replace('\n', '').split('=') + if data[0] == 'new_routers': + return data[1].replace("'", '').split(" ")[0] + + return None + +def parse_ppp_nexthop(ifname): + nexthop_file = os.path.join(directories['ppp_nexthop_dir'], ifname) + + if not os.path.exists(nexthop_file): + return False + + with open(nexthop_file, 'r') as f: + return f.read() diff --git a/smoketest/config-tests/bgp-rpki b/smoketest/config-tests/bgp-rpki index 587de67c6..657d4abcc 100644 --- a/smoketest/config-tests/bgp-rpki +++ b/smoketest/config-tests/bgp-rpki @@ -13,6 +13,7 @@ set policy route-map ebgp-transit-rpki rule 30 set local-preference '100' set policy route-map ebgp-transit-rpki rule 40 action 'permit' set policy route-map ebgp-transit-rpki rule 40 set extcommunity rt '192.0.2.100:100' set policy route-map ebgp-transit-rpki rule 40 set extcommunity soo '64500:100' +set protocols bgp address-family ipv4-unicast redistribute table 100 set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast nexthop-self set protocols bgp neighbor 1.2.3.4 address-family ipv4-unicast route-map import 'ebgp-transit-rpki' set protocols bgp neighbor 1.2.3.4 remote-as '10' diff --git a/smoketest/config-tests/dialup-router-complex b/smoketest/config-tests/dialup-router-complex index c356c73c0..12edcfef2 100644 --- a/smoketest/config-tests/dialup-router-complex +++ b/smoketest/config-tests/dialup-router-complex @@ -695,6 +695,7 @@ set service dns forwarding ignore-hosts-file set service dns forwarding listen-address '172.16.254.30' set service dns forwarding listen-address '172.31.0.254' set service dns forwarding negative-ttl '60' +set service lldp interface pppoe0 mode 'disable' set service lldp legacy-protocols cdp set service lldp snmp set service mdns repeater interface 'eth0.35' diff --git a/smoketest/config-tests/nat-basic b/smoketest/config-tests/nat-basic index ba2b1b838..f1cc0121d 100644 --- a/smoketest/config-tests/nat-basic +++ b/smoketest/config-tests/nat-basic @@ -60,7 +60,7 @@ set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 range 0 stop '192.168.189.254' set service dhcp-server shared-network-name LAN subnet 192.168.189.0/24 subnet-id '1' set service lldp interface all -set service lldp interface eth1 disable +set service lldp interface eth1 mode 'disable' set service ntp allow-client address '192.168.189.0/24' set service ntp listen-address '192.168.189.1' set service ntp server time1.vyos.net diff --git a/smoketest/configs/bgp-rpki b/smoketest/configs/bgp-rpki index 5588f15c9..2d136d545 100644 --- a/smoketest/configs/bgp-rpki +++ b/smoketest/configs/bgp-rpki @@ -46,6 +46,13 @@ policy { } protocols { bgp 64500 { + address-family { + ipv4-unicast { + redistribute { + table 100 + } + } + } neighbor 1.2.3.4 { address-family { ipv4-unicast { diff --git a/smoketest/configs/dialup-router-complex b/smoketest/configs/dialup-router-complex index aa9837fe9..018379bcd 100644 --- a/smoketest/configs/dialup-router-complex +++ b/smoketest/configs/dialup-router-complex @@ -1392,6 +1392,9 @@ service { } } lldp { + interface pppoe0 { + disable + } legacy-protocols { cdp } diff --git a/smoketest/scripts/cli/base_vyostest_shim.py b/smoketest/scripts/cli/base_vyostest_shim.py index a89b8dce5..edf940efd 100644 --- a/smoketest/scripts/cli/base_vyostest_shim.py +++ b/smoketest/scripts/cli/base_vyostest_shim.py @@ -183,6 +183,15 @@ class VyOSUnitTestSHIM: break self.assertTrue(not matched if inverse else matched, msg=search) + def verify_nftables_chain_exists(self, table, chain, inverse=False): + try: + cmd(f'sudo nft list chain {table} {chain}') + if inverse: + self.fail(f'Chain exists: {table} {chain}') + except OSError: + if not inverse: + self.fail(f'Chain does not exist: {table} {chain}') + # Verify ip rule output def verify_rules(self, rules_search, inverse=False, addr_family='inet'): rule_output = cmd(f'ip -family {addr_family} rule show') diff --git a/smoketest/scripts/cli/test_firewall.py b/smoketest/scripts/cli/test_firewall.py index 10301831e..33144c7fa 100755 --- a/smoketest/scripts/cli/test_firewall.py +++ b/smoketest/scripts/cli/test_firewall.py @@ -119,6 +119,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.com']) self.cli_set(['firewall', 'group', 'domain-group', 'smoketest_domain', 'address', 'example.org']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'eth0']) + self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'pod-smoketest']) self.cli_set(['firewall', 'group', 'interface-group', 'smoketest_interface', 'interface', 'vtun0']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'accept']) @@ -133,6 +134,9 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'action', 'accept']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '4', 'outbound-interface', 'group', '!smoketest_interface']) + # Create container network so test won't fail + self.cli_set(['container', 'network', 'smoketest', 'prefix', '10.0.0.0/24']) + self.cli_commit() self.wait_for_domain_resolver('ip vyos_filter', 'D_smoketest_domain', '192.0.2.5') @@ -654,6 +658,13 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.verify_nftables(nftables_search, 'ip vyos_filter') + # T7148 - Ensure bridge rule reject -> drop + self.cli_set(['firewall', 'global-options', 'state-policy', 'invalid', 'action', 'reject']) + self.cli_commit() + + self.verify_nftables([['ct state invalid', 'reject']], 'ip vyos_filter') + self.verify_nftables([['ct state invalid', 'drop']], 'bridge vyos_filter') + # Check conntrack is enabled from state-policy self.verify_nftables_chain([['accept']], 'ip vyos_conntrack', 'FW_CONNTRACK') self.verify_nftables_chain([['accept']], 'ip6 vyos_conntrack', 'FW_CONNTRACK') @@ -1167,7 +1178,7 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'prerouting', 'raw', 'rule', '1', 'jump-target', 'smoketest-ipsec-in4']) - + self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'output', 'filter', 'rule', '1', 'jump-target', 'smoketest-ipsec-out4']) self.cli_set(['firewall', 'ipv4', 'forward', 'filter', 'rule', '1', 'action', 'jump']) @@ -1202,8 +1213,8 @@ class TestFirewall(VyOSUnitTestSHIM.TestCase): self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'action', 'jump']) self.cli_set(['firewall', 'ipv4', 'name', 'smoketest-cycle-3', 'rule', '1', 'jump-target', 'smoketest-cycle-1']) - # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. - # It should be caught by conf validation. + # nft will fail to load cyclic jumps in any form, whether the rule is reachable or not. + # It should be caught by conf validation. with self.assertRaises(ConfigSessionError): self.cli_commit() diff --git a/smoketest/scripts/cli/test_interfaces_bonding.py b/smoketest/scripts/cli/test_interfaces_bonding.py index 1a72f9dc4..f99fd0363 100755 --- a/smoketest/scripts/cli/test_interfaces_bonding.py +++ b/smoketest/scripts/cli/test_interfaces_bonding.py @@ -167,18 +167,25 @@ class BondingInterfaceTest(BasicInterfaceTest.TestCase): def test_bonding_multi_use_member(self): # Define available bonding hash policies - for interface in ['bond10', 'bond20']: + bonds = ['bond10', 'bond20', 'bond30'] + for interface in bonds: for member in self._members: self.cli_set(self._base_path + [interface, 'member', 'interface', member]) # check validate() - can not use the same member interfaces multiple times with self.assertRaises(ConfigSessionError): self.cli_commit() - - self.cli_delete(self._base_path + ['bond20']) + # only keep the first bond interface configuration + for interface in bonds[1:]: + self.cli_delete(self._base_path + [interface]) self.cli_commit() + bond = bonds[0] + member_ifaces = read_file(f'/sys/class/net/{bond}/bonding/slaves').split() + for member in self._members: + self.assertIn(member, member_ifaces) + def test_bonding_source_interface(self): # Re-use member interface that is already a source-interface bond = 'bond99' diff --git a/smoketest/scripts/cli/test_interfaces_bridge.py b/smoketest/scripts/cli/test_interfaces_bridge.py index 54c981adc..4041b3ef3 100755 --- a/smoketest/scripts/cli/test_interfaces_bridge.py +++ b/smoketest/scripts/cli/test_interfaces_bridge.py @@ -158,6 +158,21 @@ class BridgeInterfaceTest(BasicInterfaceTest.TestCase): # verify member is assigned to the bridge self.assertEqual(interface, tmp['master']) + def test_bridge_multi_use_member(self): + # Define available bonding hash policies + bridges = ['br10', 'br20', 'br30'] + for interface in bridges: + for member in self._members: + self.cli_set(self._base_path + [interface, 'member', 'interface', member]) + + # check validate() - can not use the same member interfaces multiple times + with self.assertRaises(ConfigSessionError): + self.cli_commit() + # only keep the first bond interface configuration + for interface in bridges[1:]: + self.cli_delete(self._base_path + [interface]) + + self.cli_commit() def test_add_remove_bridge_member(self): # Add member interfaces to bridge and set STP cost/priority diff --git a/smoketest/scripts/cli/test_load-balancing_haproxy.py b/smoketest/scripts/cli/test_load-balancing_haproxy.py index 9f412aa95..077f1974f 100755 --- a/smoketest/scripts/cli/test_load-balancing_haproxy.py +++ b/smoketest/scripts/cli/test_load-balancing_haproxy.py @@ -521,5 +521,53 @@ class TestLoadBalancingReverseProxy(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError) as e: self.cli_commit() + def test_11_lb_haproxy_timeout(self): + t_default_check = '5' + t_default_client = '50' + t_default_connect = '10' + t_default_server ='50' + t_check = '4' + t_client = '300' + t_connect = '12' + t_server ='120' + t_front_client = '600' + + self.base_config() + self.cli_commit() + # Check default timeout options + config_entries = ( + f'timeout check {t_default_check}s', + f'timeout connect {t_default_connect}s', + f'timeout client {t_default_client}s', + f'timeout server {t_default_server}s', + ) + # Check default timeout options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + + # Set custom timeout options + self.cli_set(base_path + ['timeout', 'check', t_check]) + self.cli_set(base_path + ['timeout', 'client', t_client]) + self.cli_set(base_path + ['timeout', 'connect', t_connect]) + self.cli_set(base_path + ['timeout', 'server', t_server]) + self.cli_set(base_path + ['service', 'https_front', 'timeout', 'client', t_front_client]) + + self.cli_commit() + + # Check custom timeout options + config_entries = ( + f'timeout check {t_check}s', + f'timeout connect {t_connect}s', + f'timeout client {t_client}s', + f'timeout server {t_server}s', + f'timeout client {t_front_client}s', + ) + + # Check configured options + config = read_file(HAPROXY_CONF) + for config_entry in config_entries: + self.assertIn(config_entry, config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_load-balancing_wan.py b/smoketest/scripts/cli/test_load-balancing_wan.py index 92b4000b8..32e5f6915 100755 --- a/smoketest/scripts/cli/test_load-balancing_wan.py +++ b/smoketest/scripts/cli/test_load-balancing_wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright (C) 2022-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,10 +14,13 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import os import unittest import time from base_vyostest_shim import VyOSUnitTestSHIM +from vyos.utils.file import chmod_755 +from vyos.utils.file import write_file from vyos.utils.process import call from vyos.utils.process import cmd @@ -54,6 +57,16 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_delete(base_path) self.cli_commit() + removed_chains = [ + 'wlb_mangle_isp_veth1', + 'wlb_mangle_isp_veth2', + 'wlb_mangle_isp_eth201', + 'wlb_mangle_isp_eth202' + ] + + for chain in removed_chains: + self.verify_nftables_chain_exists('ip vyos_wanloadbalance', chain, inverse=True) + def test_table_routes(self): ns1 = 'ns201' ns2 = 'ns202' @@ -93,6 +106,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): cmd_in_netns(ns3, 'ip link set dev eth0 up') # Set load-balancing configuration + self.cli_set(base_path + ['wan', 'hook', '/bin/true']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'failure-count', '2']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'nexthop', '203.0.113.1']) self.cli_set(base_path + ['wan', 'interface-health', iface1, 'success-count', '1']) @@ -102,7 +116,8 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) - + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) # commit changes self.cli_commit() @@ -127,7 +142,6 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns3) def test_check_chains(self): - ns1 = 'nsA' ns2 = 'nsB' ns3 = 'nsC' @@ -137,43 +151,28 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): container_iface1 = 'ceth0' container_iface2 = 'ceth1' container_iface3 = 'ceth2' - mangle_isp1 = """table ip mangle { - chain ISP_veth1 { - counter ct mark set 0xc9 - counter meta mark set 0xc9 - counter accept + mangle_isp1 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth1 { + meta mark set 0x000000c9 ct mark set 0x000000c9 counter accept } }""" - mangle_isp2 = """table ip mangle { - chain ISP_veth2 { - counter ct mark set 0xca - counter meta mark set 0xca - counter accept + mangle_isp2 = """table ip vyos_wanloadbalance { + chain wlb_mangle_isp_veth2 { + meta mark set 0x000000ca ct mark set 0x000000ca counter accept } }""" - mangle_prerouting = """table ip mangle { - chain PREROUTING { + mangle_prerouting = """table ip vyos_wanloadbalance { + chain wlb_mangle_prerouting { type filter hook prerouting priority mangle; policy accept; - counter jump WANLOADBALANCE_PRE - } -}""" - mangle_wanloadbalance_pre = """table ip mangle { - chain WANLOADBALANCE_PRE { - iifname "veth3" ip saddr 198.51.100.0/24 ct state new meta random & 2147483647 < 1073741824 counter jump ISP_veth1 - iifname "veth3" ip saddr 198.51.100.0/24 ct state new counter jump ISP_veth2 + iifname "veth3" ip saddr 198.51.100.0/24 ct state new limit rate 5/second burst 5 packets counter numgen random mod 11 vmap { 0 : jump wlb_mangle_isp_veth1, 1-10 : jump wlb_mangle_isp_veth2 } iifname "veth3" ip saddr 198.51.100.0/24 counter meta mark set ct mark } }""" - nat_wanloadbalance = """table ip nat { - chain WANLOADBALANCE { - ct mark 0xc9 counter snat to 203.0.113.10 - ct mark 0xca counter snat to 192.0.2.10 - } -}""" - nat_vyos_pre_snat_hook = """table ip nat { - chain VYOS_PRE_SNAT_HOOK { + nat_wanloadbalance = """table ip vyos_wanloadbalance { + chain wlb_nat_postrouting { type nat hook postrouting priority srcnat - 1; policy accept; - counter jump WANLOADBALANCE + ct mark 0x000000c9 counter snat to 203.0.113.10 + ct mark 0x000000ca counter snat to 192.0.2.10 } }""" @@ -214,7 +213,7 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', iface3]) self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface1]) - self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', iface2, 'weight', '10']) # commit changes self.cli_commit() @@ -222,25 +221,19 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): time.sleep(5) # Check mangle chains - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface1}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface1}') self.assertEqual(tmp, mangle_isp1) - tmp = cmd(f'sudo nft -s list chain mangle ISP_{iface2}') + tmp = cmd(f'sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_isp_{iface2}') self.assertEqual(tmp, mangle_isp2) - tmp = cmd(f'sudo nft -s list chain mangle PREROUTING') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_mangle_prerouting') self.assertEqual(tmp, mangle_prerouting) - tmp = cmd(f'sudo nft -s list chain mangle WANLOADBALANCE_PRE') - self.assertEqual(tmp, mangle_wanloadbalance_pre) - # Check nat chains - tmp = cmd(f'sudo nft -s list chain nat WANLOADBALANCE') + tmp = cmd('sudo nft -s list chain ip vyos_wanloadbalance wlb_nat_postrouting') self.assertEqual(tmp, nat_wanloadbalance) - tmp = cmd(f'sudo nft -s list chain nat VYOS_PRE_SNAT_HOOK') - self.assertEqual(tmp, nat_vyos_pre_snat_hook) - # Delete veth interfaces and netns for iface in [iface1, iface2, iface3]: call(f'sudo ip link del dev {iface}') @@ -249,6 +242,85 @@ class TestLoadBalancingWan(VyOSUnitTestSHIM.TestCase): delete_netns(ns2) delete_netns(ns3) + def test_criteria_failover_hook(self): + isp1_iface = 'eth0' + isp2_iface = 'eth1' + lan_iface = 'eth2' + + hook_path = '/tmp/wlb_hook.sh' + hook_output_path = '/tmp/wlb_hook_output' + hook_script = f""" +#!/bin/sh + +ifname=$WLB_INTERFACE_NAME +state=$WLB_INTERFACE_STATE + +echo "$ifname - $state" > {hook_output_path} +""" + + write_file(hook_path, hook_script) + chmod_755(hook_path) + + self.cli_set(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_set(['interfaces', 'ethernet', isp2_iface, 'address', '192.0.2.2/30']) + self.cli_set(['interfaces', 'ethernet', lan_iface, 'address', '198.51.100.2/30']) + + self.cli_set(base_path + ['wan', 'hook', hook_path]) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'nexthop', '203.0.113.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp1_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'failure-count', '1']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'nexthop', '192.0.2.2']) + self.cli_set(base_path + ['wan', 'interface-health', isp2_iface, 'success-count', '1']) + self.cli_set(base_path + ['wan', 'rule', '5', 'exclude']) + self.cli_set(base_path + ['wan', 'rule', '5', 'inbound-interface', 'eth*']) + self.cli_set(base_path + ['wan', 'rule', '5', 'destination', 'address', '10.0.0.0/8']) + self.cli_set(base_path + ['wan', 'rule', '10', 'failover']) + self.cli_set(base_path + ['wan', 'rule', '10', 'inbound-interface', lan_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'protocol', 'udp']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'address', '198.51.100.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'source', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'address', '192.0.2.0/24']) + self.cli_set(base_path + ['wan', 'rule', '10', 'destination', 'port', '53']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface]) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp1_iface, 'weight', '10']) + self.cli_set(base_path + ['wan', 'rule', '10', 'interface', isp2_iface]) + + # commit changes + self.cli_commit() + + time.sleep(5) + + # Verify isp1 + criteria + + nftables_search = [ + [f'iifname "eth*"', 'ip daddr 10.0.0.0/8', 'return'], + [f'iifname "{lan_iface}"', 'ip saddr 198.51.100.0/24', 'udp sport 53', 'ip daddr 192.0.2.0/24', 'udp dport 53', f'jump wlb_mangle_isp_{isp1_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Trigger failure on isp1 health check + + self.cli_delete(['interfaces', 'ethernet', isp1_iface, 'address', '203.0.113.2/30']) + self.cli_commit() + + time.sleep(10) + + # Verify failover to isp2 + + nftables_search = [ + [f'iifname "{lan_iface}"', f'jump wlb_mangle_isp_{isp2_iface}'] + ] + + self.verify_nftables_chain(nftables_search, 'ip vyos_wanloadbalance', 'wlb_mangle_prerouting') + + # Verify hook output + + self.assertTrue(os.path.exists(hook_output_path)) + + with open(hook_output_path, 'r') as f: + self.assertIn('eth0 - FAILED', f.read()) if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_policy.py b/smoketest/scripts/cli/test_policy.py index 9d4fc0845..985097726 100755 --- a/smoketest/scripts/cli/test_policy.py +++ b/smoketest/scripts/cli/test_policy.py @@ -1149,6 +1149,16 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): }, }, }, + 'vrf-match': { + 'rule': { + '10': { + 'action': 'permit', + 'match': { + 'source-vrf': 'TEST', + }, + }, + }, + }, } self.cli_set(['policy', 'access-list', access_list, 'rule', '10', 'action', 'permit']) @@ -1260,6 +1270,8 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): self.cli_set(path + ['rule', rule, 'match', 'rpki', 'valid']) if 'protocol' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'protocol', rule_config['match']['protocol']]) + if 'source-vrf' in rule_config['match']: + self.cli_set(path + ['rule', rule, 'match', 'source-vrf', rule_config['match']['source-vrf']]) if 'tag' in rule_config['match']: self.cli_set(path + ['rule', rule, 'match', 'tag', rule_config['match']['tag']]) @@ -1438,6 +1450,9 @@ class TestPolicy(VyOSUnitTestSHIM.TestCase): if 'rpki-valid' in rule_config['match']: tmp = f'match rpki valid' self.assertIn(tmp, config) + if 'source-vrf' in rule_config['match']: + tmp = f'match source-vrf {rule_config["match"]["source-vrf"]}' + self.assertIn(tmp, config) if 'tag' in rule_config['match']: tmp = f'match tag {rule_config["match"]["tag"]}' self.assertIn(tmp, config) diff --git a/smoketest/scripts/cli/test_protocols_babel.py b/smoketest/scripts/cli/test_protocols_babel.py index 7ecf54600..3a9ee2d62 100755 --- a/smoketest/scripts/cli/test_protocols_babel.py +++ b/smoketest/scripts/cli/test_protocols_babel.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2024 VyOS maintainers and contributors +# Copyright (C) 2024-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -72,7 +72,7 @@ class TestProtocolsBABEL(VyOSUnitTestSHIM.TestCase): self.assertIn(f' babel smoothing-half-life {smoothing_half_life}', frrconfig) def test_02_redistribute(self): - ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'rip', 'static'] + ipv4_protos = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] ipv6_protos = ['bgp', 'connected', 'isis', 'kernel', 'ospfv3', 'ripng', 'static'] self.cli_set(base_path + ['interface', self._interfaces[0], 'enable-timestamps']) diff --git a/smoketest/scripts/cli/test_protocols_bgp.py b/smoketest/scripts/cli/test_protocols_bgp.py index 0eda52ff6..d8d5415b5 100755 --- a/smoketest/scripts/cli/test_protocols_bgp.py +++ b/smoketest/scripts/cli/test_protocols_bgp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -672,6 +672,10 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'metric' : '400', 'route_map' : 'redistr-ipv4-kernel', }, + 'nhrp': { + 'metric': '400', + 'route_map': 'redistr-ipv4-nhrp', + }, 'ospf' : { 'metric' : '500', 'route_map' : 'redistr-ipv4-ospf', @@ -685,14 +689,30 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'route_map' : 'redistr-ipv4-static', }, 'table' : { - 'number' : ['10', '20', '30', '40'], + '10' : { + 'metric' : '810', + 'route_map' : 'redistr-ipv4-table-10', + }, + '20' : { + 'metric' : '820', + 'route_map' : 'redistr-ipv4-table-20', + }, + '30' : { + 'metric' : '830', + 'route_map' : 'redistr-ipv4-table-30', + }, }, } for proto, proto_config in redistributes.items(): proto_path = base_path + ['address-family', 'ipv4-unicast', 'redistribute', proto] - if proto == 'table' and 'number' in proto_config: - for number in proto_config['number']: - self.cli_set(proto_path, value=number) + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) else: self.cli_set(proto_path) if 'metric' in proto_config: @@ -723,9 +743,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(' address-family ipv4 unicast', frrconfig) for proto, proto_config in redistributes.items(): - if proto == 'table' and 'number' in proto_config: - for number in proto_config['number']: - self.assertIn(f' redistribute table-direct {number}', frrconfig) + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) else: tmp = f' redistribute {proto}' if 'metric' in proto_config: @@ -794,14 +821,30 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): 'route_map' : 'redistr-ipv6-static', }, 'table' : { - 'number' : ['100', '120', '130', '140'], + '110' : { + 'metric' : '811', + 'route_map' : 'redistr-ipv6-table-110', + }, + '120' : { + 'metric' : '821', + 'route_map' : 'redistr-ipv6-table-120', + }, + '130' : { + 'metric' : '831', + 'route_map' : 'redistr-ipv6-table-130', + }, }, } for proto, proto_config in redistributes.items(): proto_path = base_path + ['address-family', 'ipv6-unicast', 'redistribute', proto] - if proto == 'table' and 'number' in proto_config: - for number in proto_config['number']: - self.cli_set(proto_path, value=number) + if proto == 'table': + for table, table_config in proto_config.items(): + self.cli_set(proto_path + [table]) + if 'metric' in table_config: + self.cli_set(proto_path + [table, 'metric'], value=table_config['metric']) + if 'route_map' in table_config: + self.cli_set(['policy', 'route-map', table_config['route_map'], 'rule', '10', 'action'], value='permit') + self.cli_set(proto_path + [table, 'route-map'], value=table_config['route_map']) else: self.cli_set(proto_path) if 'metric' in proto_config: @@ -829,9 +872,16 @@ class TestProtocolsBGP(VyOSUnitTestSHIM.TestCase): self.assertIn(' no bgp ebgp-requires-policy', frrconfig) for proto, proto_config in redistributes.items(): - if proto == 'table' and 'number' in proto_config: - for number in proto_config['number']: - self.assertIn(f' redistribute table-direct {number}', frrconfig) + if proto == 'table': + for table, table_config in proto_config.items(): + tmp = f' redistribute table-direct {table}' + if 'metric' in proto_config: + metric = proto_config['metric'] + tmp += f' metric {metric}' + if 'route_map' in proto_config: + route_map = proto_config['route_map'] + tmp += f' route-map {route_map}' + self.assertIn(tmp, frrconfig) else: # FRR calls this OSPF6 if proto == 'ospfv3': diff --git a/smoketest/scripts/cli/test_protocols_isis.py b/smoketest/scripts/cli/test_protocols_isis.py index 598250d28..14e833fd9 100755 --- a/smoketest/scripts/cli/test_protocols_isis.py +++ b/smoketest/scripts/cli/test_protocols_isis.py @@ -59,7 +59,7 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): route_map = 'EXPORT-ISIS' rule = '10' metric_style = 'transition' - + redistribute = ['babel', 'bgp', 'connected', 'kernel', 'nhrp', 'ospf', 'rip', 'static'] self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'action', 'permit']) self.cli_set(['policy', 'prefix-list', prefix_list, 'rule', rule, 'prefix', '203.0.113.0/24']) self.cli_set(['policy', 'route-map', route_map, 'rule', rule, 'action', 'permit']) @@ -80,7 +80,9 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() - self.cli_set(base_path + ['redistribute', 'ipv4', 'connected', 'level-2', 'route-map', route_map]) + for proto in redistribute: + self.cli_set(base_path + ['redistribute', 'ipv4', proto, 'level-2', 'route-map', route_map]) + self.cli_set(base_path + ['metric-style', metric_style]) self.cli_set(base_path + ['log-adjacency-changes']) @@ -92,7 +94,8 @@ class TestProtocolsISIS(VyOSUnitTestSHIM.TestCase): self.assertIn(f' net {net}', tmp) self.assertIn(f' metric-style {metric_style}', tmp) self.assertIn(f' log-adjacency-changes', tmp) - self.assertIn(f' redistribute ipv4 connected level-2 route-map {route_map}', tmp) + for proto in redistribute: + self.assertIn(f' redistribute ipv4 {proto} level-2 route-map {route_map}', tmp) for interface in self._interfaces: tmp = self.getFRRconfig(f'interface {interface}', endsection='^exit') diff --git a/smoketest/scripts/cli/test_protocols_ospf.py b/smoketest/scripts/cli/test_protocols_ospf.py index 77882737f..ea55fa031 100755 --- a/smoketest/scripts/cli/test_protocols_ospf.py +++ b/smoketest/scripts/cli/test_protocols_ospf.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2024 VyOS maintainers and contributors +# Copyright (C) 2021-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -255,7 +255,7 @@ class TestProtocolsOSPF(VyOSUnitTestSHIM.TestCase): def test_ospf_07_redistribute(self): metric = '15' metric_type = '1' - redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'rip', 'static'] + redistribute = ['babel', 'bgp', 'connected', 'isis', 'kernel', 'nhrp', 'rip', 'static'] for protocol in redistribute: self.cli_set(base_path + ['redistribute', protocol, 'metric', metric]) diff --git a/smoketest/scripts/cli/test_protocols_rip.py b/smoketest/scripts/cli/test_protocols_rip.py index 671ef8cd5..27b543803 100755 --- a/smoketest/scripts/cli/test_protocols_rip.py +++ b/smoketest/scripts/cli/test_protocols_rip.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2021-2023 VyOS maintainers and contributors +# Copyright (C) 2021-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -82,7 +82,7 @@ class TestProtocolsRIP(VyOSUnitTestSHIM.TestCase): interfaces = Section.interfaces('ethernet') neighbors = ['1.2.3.4', '1.2.3.5', '1.2.3.6'] networks = ['10.0.0.0/8', '172.16.0.0/12', '192.168.0.0/16'] - redistribute = ['bgp', 'connected', 'isis', 'kernel', 'ospf', 'static'] + redistribute = ['bgp', 'connected', 'isis', 'kernel', 'nhrp', 'ospf', 'static'] timer_garbage = '888' timer_timeout = '1000' timer_update = '90' diff --git a/smoketest/scripts/cli/test_protocols_rpki.py b/smoketest/scripts/cli/test_protocols_rpki.py index ef2f30d3e..0addf7fee 100755 --- a/smoketest/scripts/cli/test_protocols_rpki.py +++ b/smoketest/scripts/cli/test_protocols_rpki.py @@ -248,5 +248,41 @@ class TestProtocolsRPKI(VyOSUnitTestSHIM.TestCase): with self.assertRaises(ConfigSessionError): self.cli_commit() + def test_rpki_source_address(self): + peer = '192.0.2.1' + port = '8080' + preference = '1' + username = 'foo' + source_address = '100.10.10.1' + + self.cli_set(['interfaces', 'ethernet', 'eth0', 'address', f'{source_address}/24']) + + # Configure a TCP cache server + self.cli_set(base_path + ['cache', peer, 'port', port]) + self.cli_set(base_path + ['cache', peer, 'preference', preference]) + self.cli_set(base_path + ['cache', peer, 'source-address', source_address]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn(f'rpki cache tcp {peer} {port} source {source_address} preference {preference}', frrconfig) + + self.cli_set(['pki', 'openssh', rpki_key_name, 'private', 'key', rpki_ssh_key.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'key', rpki_ssh_pub.replace('\n', '')]) + self.cli_set(['pki', 'openssh', rpki_key_name, 'public', 'type', rpki_key_type]) + + # Configure a SSH cache server + self.cli_set(base_path + ['cache', peer, 'ssh', 'username', username]) + self.cli_set(base_path + ['cache', peer, 'ssh', 'key', rpki_key_name]) + self.cli_commit() + + # Verify FRR configuration + frrconfig = self.getFRRconfig('rpki') + self.assertIn( + f'rpki cache ssh {peer} {port} {username} /run/frr/id_rpki_{peer} /run/frr/id_rpki_{peer}.pub source {source_address} preference {preference}', + frrconfig, + ) + + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/smoketest/scripts/cli/test_service_ipoe-server.py b/smoketest/scripts/cli/test_service_ipoe-server.py index 67e8ca93f..3b3c205cd 100755 --- a/smoketest/scripts/cli/test_service_ipoe-server.py +++ b/smoketest/scripts/cli/test_service_ipoe-server.py @@ -260,7 +260,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" tmp = ','.join(vlans) self.assertIn(f'{interface},{tmp}', conf['ipoe']['vlan-mon']) - def test_ipoe_server_static_client_ip(self): + def test_ipoe_server_static_client_ip_address(self): mac_address = '08:00:27:2f:d8:06' ip_address = '192.0.2.100' @@ -274,7 +274,7 @@ delegate={delegate_2_prefix},{delegate_mask},name={pool_name}""" interface, 'mac', mac_address, - 'static-ip', + 'ip-address', ip_address, ] ) diff --git a/smoketest/scripts/cli/test_service_lldp.py b/smoketest/scripts/cli/test_service_lldp.py index 9d72ef78f..c73707e0d 100755 --- a/smoketest/scripts/cli/test_service_lldp.py +++ b/smoketest/scripts/cli/test_service_lldp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2022-2024 VyOS maintainers and contributors +# Copyright (C) 2022-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -117,6 +117,8 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): config = read_file(LLDPD_CONF) self.assertIn(f'configure ports {interface} med location elin "{elin}"', config) + # This is the CLI default mode + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) self.assertIn(f'configure system interface pattern "{interface}"', config) def test_06_lldp_snmp(self): @@ -134,5 +136,50 @@ class TestServiceLLDP(VyOSUnitTestSHIM.TestCase): self.cli_delete(['service', 'snmp']) + def test_07_lldp_interface_mode(self): + interfaces = Section.interfaces('ethernet', vlan=False) + + # set interface mode to 'tx' + self.cli_set(base_path + ['interface', 'all']) + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'disable']) + # commit changes + self.cli_commit() + + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status disable', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-only', config) + + # Change configuration to tx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status tx-only', config) + + # Change configuration to rx-only + for interface in interfaces: + self.cli_set(base_path + ['interface', interface, 'mode', 'rx-tx']) + # commit changes + self.cli_commit() + # verify configuration + config = read_file(LLDPD_CONF) + for interface in interfaces: + self.assertIn(f'configure ports {interface} lldp status rx-and-tx', config) + if __name__ == '__main__': unittest.main(verbosity=2) diff --git a/src/conf_mode/container.py b/src/conf_mode/container.py index 594de3eb0..3636b0871 100755 --- a/src/conf_mode/container.py +++ b/src/conf_mode/container.py @@ -22,6 +22,7 @@ from ipaddress import ip_address from ipaddress import ip_network from json import dumps as json_write +import psutil from vyos.base import Warning from vyos.config import Config from vyos.configdict import dict_merge @@ -223,6 +224,21 @@ def verify(container): if not os.path.exists(source): raise ConfigError(f'Volume "{volume}" source path "{source}" does not exist!') + if 'tmpfs' in container_config: + for tmpfs, tmpfs_config in container_config['tmpfs'].items(): + if 'destination' not in tmpfs_config: + raise ConfigError(f'tmpfs "{tmpfs}" has no destination path configured!') + if 'size' in tmpfs_config: + free_mem_mb: int = psutil.virtual_memory().available / 1024 / 1024 + if int(tmpfs_config['size']) > free_mem_mb: + Warning(f'tmpfs "{tmpfs}" size is greater than the current free memory!') + + total_mem_mb: int = (psutil.virtual_memory().total / 1024 / 1024) / 2 + if int(tmpfs_config['size']) > total_mem_mb: + raise ConfigError(f'tmpfs "{tmpfs}" size should not be more than 50% of total system memory!') + else: + raise ConfigError(f'tmpfs "{tmpfs}" has no size configured!') + if 'port' in container_config: for tmp in container_config['port']: if not {'source', 'destination'} <= set(container_config['port'][tmp]): @@ -362,6 +378,14 @@ def generate_run_arguments(name, container_config): prop = vol_config['propagation'] volume += f' --volume {svol}:{dvol}:{mode},{prop}' + # Mount tmpfs + tmpfs = '' + if 'tmpfs' in container_config: + for tmpfs_config in container_config['tmpfs'].values(): + dest = tmpfs_config['destination'] + size = tmpfs_config['size'] + tmpfs += f' --mount=type=tmpfs,tmpfs-size={size}M,destination={dest}' + host_pid = '' if 'allow_host_pid' in container_config: host_pid = '--pid host' @@ -373,7 +397,7 @@ def generate_run_arguments(name, container_config): container_base_cmd = f'--detach --interactive --tty --replace {capabilities} --cpus {cpu_quota} {sysctl_opt} ' \ f'--memory {memory}m --shm-size {shared_memory}m --memory-swap 0 --restart {restart} ' \ - f'--name {name} {hostname} {device} {port} {name_server} {volume} {env_opt} {label} {uid} {host_pid}' + f'--name {name} {hostname} {device} {port} {name_server} {volume} {tmpfs} {env_opt} {label} {uid} {host_pid}' entrypoint = '' if 'entrypoint' in container_config: diff --git a/src/conf_mode/interfaces_bonding.py b/src/conf_mode/interfaces_bonding.py index 4f1141dcb..84316c16e 100755 --- a/src/conf_mode/interfaces_bonding.py +++ b/src/conf_mode/interfaces_bonding.py @@ -126,9 +126,8 @@ def get_config(config=None): # Restore existing config level conf.set_level(old_level) - if dict_search('member.interface', bond): - for interface, interface_config in bond['member']['interface'].items(): - + if dict_search('member.interface', bond) is not None: + for interface in bond['member']['interface']: interface_ethernet_config = conf.get_config_dict( ['interfaces', 'ethernet', interface], key_mangling=('-', '_'), @@ -137,44 +136,45 @@ def get_config(config=None): with_defaults=False, with_recursive_defaults=False) - interface_config['config_paths'] = dict_to_paths_values(interface_ethernet_config) + bond['member']['interface'][interface].update({'config_paths' : + dict_to_paths_values(interface_ethernet_config)}) # Check if member interface is a new member if not conf.exists_effective(base + [ifname, 'member', 'interface', interface]): bond['shutdown_required'] = {} - interface_config['new_added'] = {} + bond['member']['interface'][interface].update({'new_added' : {}}) # Check if member interface is disabled conf.set_level(['interfaces']) section = Section.section(interface) # this will be 'ethernet' for 'eth0' if conf.exists([section, interface, 'disable']): - interface_config['disable'] = '' + if tmp: bond['member']['interface'][interface].update({'disable': ''}) conf.set_level(old_level) # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp: interface_config['is_bridge_member'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') - for tmp in is_member(conf, interface, 'bonding'): - if bond['ifname'] == tmp: - continue - interface_config['is_bond_member'] = tmp + if ifname in tmp: + del tmp[ifname] + if tmp: bond['member']['interface'][interface].update({'is_bond_member' : tmp}) # Check if member interface is used as source-interface on another interface tmp = is_source_interface(conf, interface) - if tmp: interface_config['is_source_interface'] = tmp + if tmp: bond['member']['interface'][interface].update({'is_source_interface' : tmp}) # bond members must not have an assigned address tmp = has_address_configured(conf, interface) - if tmp: interface_config['has_address'] = {} + if tmp: bond['member']['interface'][interface].update({'has_address' : ''}) # bond members must not have a VRF attached tmp = has_vrf_configured(conf, interface) - if tmp: interface_config['has_vrf'] = {} + if tmp: bond['member']['interface'][interface].update({'has_vrf' : ''}) + return bond diff --git a/src/conf_mode/interfaces_bridge.py b/src/conf_mode/interfaces_bridge.py index 637db442a..aff93af2a 100755 --- a/src/conf_mode/interfaces_bridge.py +++ b/src/conf_mode/interfaces_bridge.py @@ -74,8 +74,9 @@ def get_config(config=None): for interface in list(bridge['member']['interface']): # Check if member interface is already member of another bridge tmp = is_member(conf, interface, 'bridge') - if tmp and bridge['ifname'] not in tmp: - bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) + if ifname in tmp: + del tmp[ifname] + if tmp: bridge['member']['interface'][interface].update({'is_bridge_member' : tmp}) # Check if member interface is already member of a bond tmp = is_member(conf, interface, 'bonding') diff --git a/src/conf_mode/interfaces_geneve.py b/src/conf_mode/interfaces_geneve.py index 007708d4a..1c5b4d0e7 100755 --- a/src/conf_mode/interfaces_geneve.py +++ b/src/conf_mode/interfaces_geneve.py @@ -47,7 +47,7 @@ def get_config(config=None): # GENEVE interfaces are picky and require recreation if certain parameters # change. But a GENEVE interface should - of course - not be re-created if # it's description or IP address is adjusted. Feels somehow logic doesn't it? - for cli_option in ['remote', 'vni', 'parameters']: + for cli_option in ['remote', 'vni', 'parameters', 'port']: if is_node_changed(conf, base + [ifname, cli_option]): geneve.update({'rebuild_required': {}}) diff --git a/src/conf_mode/interfaces_wireguard.py b/src/conf_mode/interfaces_wireguard.py index 877d013cf..192937dba 100755 --- a/src/conf_mode/interfaces_wireguard.py +++ b/src/conf_mode/interfaces_wireguard.py @@ -19,6 +19,9 @@ from sys import exit from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.configdict import is_node_changed +from vyos.configdict import is_source_interface +from vyos.configdep import set_dependents +from vyos.configdep import call_dependents from vyos.configverify import verify_vrf from vyos.configverify import verify_address from vyos.configverify import verify_bridge_delete @@ -35,6 +38,7 @@ from vyos import airbag from pathlib import Path airbag.enable() + def get_config(config=None): """ Retrive CLI config as dictionary. Dictionary can never be empty, as at least the @@ -61,11 +65,25 @@ def get_config(config=None): if 'disable' not in peer_config and 'host_name' in peer_config: wireguard['peers_need_resolve'].append(peer) + # Check if interface is used as source-interface on VXLAN interface + tmp = is_source_interface(conf, ifname, 'vxlan') + if tmp: + if 'deleted' not in wireguard: + set_dependents('vxlan', conf, tmp) + else: + wireguard['is_source_interface'] = tmp + return wireguard + def verify(wireguard): if 'deleted' in wireguard: verify_bridge_delete(wireguard) + if 'is_source_interface' in wireguard: + raise ConfigError( + f'Interface "{wireguard["ifname"]}" cannot be deleted as it is used ' + f'as source interface for "{wireguard["is_source_interface"]}"!' + ) return None verify_mtu_ipv6(wireguard) @@ -119,9 +137,11 @@ def verify(wireguard): public_keys.append(peer['public_key']) + def generate(wireguard): return None + def apply(wireguard): check_kmod('wireguard') @@ -157,8 +177,11 @@ def apply(wireguard): domain_action = 'stop' call(f'systemctl {domain_action} vyos-domain-resolver.service') + call_dependents() + return None + if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/load-balancing_wan.py b/src/conf_mode/load-balancing_wan.py index 5da0b906b..92d9acfba 100755 --- a/src/conf_mode/load-balancing_wan.py +++ b/src/conf_mode/load-balancing_wan.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2023 VyOS maintainers and contributors +# Copyright (C) 2023-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -14,24 +14,16 @@ # You should have received a copy of the GNU General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. -import os - from sys import exit -from shutil import rmtree -from vyos.base import Warning from vyos.config import Config from vyos.configdep import set_dependents, call_dependents from vyos.utils.process import cmd -from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() -load_balancing_dir = '/run/load-balance' -load_balancing_conf_file = f'{load_balancing_dir}/wlb.conf' -systemd_service = 'vyos-wan-load-balance.service' - +service = 'vyos-wan-load-balance.service' def get_config(config=None): if config: @@ -40,6 +32,7 @@ def get_config(config=None): conf = Config() base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, key_mangling=('-', '_'), no_tag_node_value_mangle=True, get_first_key=True, @@ -59,87 +52,61 @@ def verify(lb): if not lb: return None - if 'interface_health' not in lb: - raise ConfigError( - 'A valid WAN load-balance configuration requires an interface with a nexthop!' - ) - - for interface, interface_config in lb['interface_health'].items(): - if 'nexthop' not in interface_config: - raise ConfigError( - f'interface-health {interface} nexthop must be specified!') - - if 'test' in interface_config: - for test_rule, test_config in interface_config['test'].items(): - if 'type' in test_config: - if test_config['type'] == 'user-defined' and 'test_script' not in test_config: - raise ConfigError( - f'test {test_rule} script must be defined for test-script!' - ) - - if 'rule' not in lb: - Warning( - 'At least one rule with an (outbound) interface must be defined for WAN load balancing to be active!' - ) + if 'interface_health' in lb: + for ifname, health_conf in lb['interface_health'].items(): + if 'nexthop' not in health_conf: + raise ConfigError(f'Nexthop must be configured for interface {ifname}') + + if 'test' not in health_conf: + continue + + for test_id, test_conf in health_conf['test'].items(): + if 'type' not in test_conf: + raise ConfigError(f'No type configured for health test on interface {ifname}') + + if test_conf['type'] == 'user-defined' and 'test_script' not in test_conf: + raise ConfigError(f'Missing user-defined script for health test on interface {ifname}') else: - for rule, rule_config in lb['rule'].items(): - if 'inbound_interface' not in rule_config: - raise ConfigError(f'rule {rule} inbound-interface must be specified!') - if {'failover', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} failover cannot be configured with exclude!') - if {'limit', 'exclude'} <= set(rule_config): - raise ConfigError(f'rule {rule} limit cannot be used with exclude!') - if 'interface' not in rule_config: - if 'exclude' not in rule_config: - Warning( - f'rule {rule} will be inactive because no (outbound) interfaces have been defined for this rule' - ) - for direction in {'source', 'destination'}: - if direction in rule_config: - if 'protocol' in rule_config and 'port' in rule_config[ - direction]: - if rule_config['protocol'] not in {'tcp', 'udp'}: - raise ConfigError('ports can only be specified when protocol is "tcp" or "udp"') + raise ConfigError('Interface health tests must be configured') + if 'rule' in lb: + for rule_id, rule_conf in lb['rule'].items(): + if 'interface' not in rule_conf and 'exclude' not in rule_conf: + raise ConfigError(f'Interface or exclude not specified on load-balancing wan rule {rule_id}') -def generate(lb): - if not lb: - # Delete /run/load-balance/wlb.conf - if os.path.isfile(load_balancing_conf_file): - os.unlink(load_balancing_conf_file) - # Delete old directories - if os.path.isdir(load_balancing_dir): - rmtree(load_balancing_dir, ignore_errors=True) - if os.path.exists('/var/run/load-balance/wlb.out'): - os.unlink('/var/run/load-balance/wlb.out') + if 'failover' in rule_conf and 'exclude' in rule_conf: + raise ConfigError(f'Failover cannot be configured with exclude on load-balancing wan rule {rule_id}') - return None + if 'limit' in rule_conf: + if 'exclude' in rule_conf: + raise ConfigError(f'Limit cannot be configured with exclude on load-balancing wan rule {rule_id}') - # Create load-balance dir - if not os.path.isdir(load_balancing_dir): - os.mkdir(load_balancing_dir) + if 'rate' in rule_conf['limit'] and 'period' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit period" on load-balancing wan rule {rule_id}') - render(load_balancing_conf_file, 'load-balancing/wlb.conf.j2', lb) + if 'period' in rule_conf['limit'] and 'rate' not in rule_conf['limit']: + raise ConfigError(f'Missing "limit rate" on load-balancing wan rule {rule_id}') - return None + for direction in ['source', 'destination']: + if direction in rule_conf: + if 'port' in rule_conf[direction]: + if 'protocol' not in rule_conf: + raise ConfigError(f'Protocol required to specify port on load-balancing wan rule {rule_id}') + + if rule_conf['protocol'] not in ['tcp', 'udp', 'tcp_udp']: + raise ConfigError(f'Protocol must be tcp, udp or tcp_udp to specify port on load-balancing wan rule {rule_id}') +def generate(lb): + return None def apply(lb): if not lb: - try: - cmd(f'systemctl stop {systemd_service}') - except Exception as e: - print(f"Error message: {e}") - + cmd(f'sudo systemctl stop {service}') else: - cmd('sudo sysctl -w net.netfilter.nf_conntrack_acct=1') - cmd(f'systemctl restart {systemd_service}') + cmd(f'sudo systemctl restart {service}') call_dependents() - return None - - if __name__ == '__main__': try: c = get_config() diff --git a/src/conf_mode/service_snmp.py b/src/conf_mode/service_snmp.py index d85f20820..c64c59af7 100755 --- a/src/conf_mode/service_snmp.py +++ b/src/conf_mode/service_snmp.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 # -# Copyright (C) 2018-2024 VyOS maintainers and contributors +# Copyright (C) 2018-2025 VyOS maintainers and contributors # # This program is free software; you can redistribute it and/or modify # it under the terms of the GNU General Public License version 2 or later as @@ -147,6 +147,9 @@ def verify(snmp): return None if 'user' in snmp['v3']: + if 'engineid' not in snmp['v3']: + raise ConfigError(f'EngineID must be configured for SNMPv3!') + for user, user_config in snmp['v3']['user'].items(): if 'group' not in user_config: raise ConfigError(f'Group membership required for user "{user}"!') diff --git a/src/conf_mode/system_sflow.py b/src/conf_mode/system_sflow.py index 41119b494..a22dac36f 100755 --- a/src/conf_mode/system_sflow.py +++ b/src/conf_mode/system_sflow.py @@ -54,7 +54,7 @@ def verify(sflow): # Check if configured sflow agent-address exist in the system if 'agent_address' in sflow: tmp = sflow['agent_address'] - if not is_addr_assigned(tmp): + if not is_addr_assigned(tmp, include_vrf=True): raise ConfigError( f'Configured "sflow agent-address {tmp}" does not exist in the system!' ) diff --git a/src/etc/netplug/vyos-netplug-dhcp-client b/src/etc/netplug/vyos-netplug-dhcp-client index 83fed70f0..4cc824afd 100755 --- a/src/etc/netplug/vyos-netplug-dhcp-client +++ b/src/etc/netplug/vyos-netplug-dhcp-client @@ -19,21 +19,22 @@ import sys from time import sleep -from vyos.configquery import ConfigTreeQuery +from vyos.config import Config from vyos.configdict import get_interface_dict from vyos.ifconfig import Interface from vyos.ifconfig import Section from vyos.utils.boot import boot_configuration_complete from vyos.utils.commit import commit_in_progress from vyos import airbag + airbag.enable() if len(sys.argv) < 3: - airbag.noteworthy("Must specify both interface and link status!") + airbag.noteworthy('Must specify both interface and link status!') sys.exit(1) if not boot_configuration_complete(): - airbag.noteworthy("System bootup not yet finished...") + airbag.noteworthy('System bootup not yet finished...') sys.exit(1) interface = sys.argv[1] @@ -47,8 +48,10 @@ while commit_in_progress(): sleep(1) in_out = sys.argv[2] -config = ConfigTreeQuery() +config = Config() interface_path = ['interfaces'] + Section.get_config_path(interface).split() -_, interface_config = get_interface_dict(config, interface_path[:-1], ifname=interface, with_pki=True) +_, interface_config = get_interface_dict( + config, interface_path[:-1], ifname=interface, with_pki=True +) Interface(interface).update(interface_config) diff --git a/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb new file mode 100755 index 000000000..fff258afa --- /dev/null +++ b/src/etc/ppp/ip-up.d/99-vyos-pppoe-wlb @@ -0,0 +1,61 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +# This is a Python hook script which is invoked whenever a PPPoE session goes +# "ip-up". It will call into our vyos.ifconfig library and will then execute +# common tasks for the PPPoE interface. The reason we have to "hook" this is +# that we can not create a pppoeX interface in advance in linux and then connect +# pppd to this already existing interface. + +import os +import signal + +from sys import argv +from sys import exit + +from vyos.defaults import directories + +# When the ppp link comes up, this script is called with the following +# parameters +# $1 the interface name used by pppd (e.g. ppp3) +# $2 the tty device name +# $3 the tty device speed +# $4 the local IP address for the interface +# $5 the remote IP address +# $6 the parameter specified by the 'ipparam' option to pppd + +if (len(argv) < 7): + exit(1) + +wlb_pid_file = '/run/wlb_daemon.pid' + +interface = argv[6] +nexthop = argv[5] + +if not os.path.exists(directories['ppp_nexthop_dir']): + os.mkdir(directories['ppp_nexthop_dir']) + +nexthop_file = os.path.join(directories['ppp_nexthop_dir'], interface) + +with open(nexthop_file, 'w') as f: + f.write(nexthop) + +# Trigger WLB daemon update +if os.path.exists(wlb_pid_file): + with open(wlb_pid_file, 'r') as f: + pid = int(f.read()) + + os.kill(pid, signal.SIGUSR2) diff --git a/src/helpers/vyos-load-balancer.py b/src/helpers/vyos-load-balancer.py new file mode 100755 index 000000000..30329fd5c --- /dev/null +++ b/src/helpers/vyos-load-balancer.py @@ -0,0 +1,312 @@ +#!/usr/bin/python3 + +# Copyright 2024-2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public +# License along with this library. If not, see <http://www.gnu.org/licenses/>. + +import json +import os +import signal +import sys +import time + +from vyos.config import Config +from vyos.template import render +from vyos.utils.commit import commit_in_progress +from vyos.utils.network import get_interface_address +from vyos.utils.process import rc_cmd +from vyos.utils.process import run +from vyos.xml_ref import get_defaults +from vyos.wanloadbalance import health_ping_host +from vyos.wanloadbalance import health_ping_host_ttl +from vyos.wanloadbalance import parse_dhcp_nexthop +from vyos.wanloadbalance import parse_ppp_nexthop + +nftables_wlb_conf = '/run/nftables_wlb.conf' +wlb_status_file = '/run/wlb_status.json' +wlb_pid_file = '/run/wlb_daemon.pid' +sleep_interval = 5 # Main loop sleep interval + +def health_check(ifname, conf, state, test_defaults): + # Run health tests for interface + + if get_ipv4_address(ifname) is None: + return False + + if 'test' not in conf: + resp_time = test_defaults['resp-time'] + target = conf['nexthop'] + + if target == 'dhcp': + target = state['dhcp_nexthop'] + + if not target: + return False + + return health_ping_host(target, ifname, wait_time=resp_time) + + for test_id, test_conf in conf['test'].items(): + check_type = test_conf['type'] + + if check_type == 'ping': + resp_time = test_conf['resp_time'] + target = test_conf['target'] + if not health_ping_host(target, ifname, wait_time=resp_time): + return False + elif check_type == 'ttl': + target = test_conf['target'] + ttl_limit = test_conf['ttl_limit'] + if not health_ping_host_ttl(target, ifname, ttl_limit=ttl_limit): + return False + elif check_type == 'user-defined': + script = test_conf['test_script'] + rc = run(script) + if rc != 0: + return False + + return True + +def on_state_change(lb, ifname, state): + # Run hook on state change + if 'hook' in lb: + script_path = os.path.join('/config/scripts/', lb['hook']) + env = { + 'WLB_INTERFACE_NAME': ifname, + 'WLB_INTERFACE_STATE': 'ACTIVE' if state else 'FAILED' + } + + code = run(script_path, env=env) + if code != 0: + print('WLB hook returned non-zero error code') + + print(f'INFO: State change: {ifname} -> {state}') + +def get_ipv4_address(ifname): + # Get primary ipv4 address on interface (for source nat) + addr_json = get_interface_address(ifname) + if addr_json and 'addr_info' in addr_json and len(addr_json['addr_info']) > 0: + for addr_info in addr_json['addr_info']: + if addr_info['family'] == 'inet': + if 'local' in addr_info: + return addr_json['addr_info'][0]['local'] + return None + +def dynamic_nexthop_update(lb, ifname): + # Update on DHCP/PPP address/nexthop changes + # Return True if nftables needs to be updated - IP change + + if 'dhcp_nexthop' in lb['health_state'][ifname]: + if ifname[:5] == 'pppoe': + dhcp_nexthop_addr = parse_ppp_nexthop(ifname) + else: + dhcp_nexthop_addr = parse_dhcp_nexthop(ifname) + + table_num = lb['health_state'][ifname]['table_number'] + + if dhcp_nexthop_addr and lb['health_state'][ifname]['dhcp_nexthop'] != dhcp_nexthop_addr: + lb['health_state'][ifname]['dhcp_nexthop'] = dhcp_nexthop_addr + run(f'ip route replace table {table_num} default dev {ifname} via {dhcp_nexthop_addr}') + + if_addr = get_ipv4_address(ifname) + if if_addr and if_addr != lb['health_state'][ifname]['if_addr']: + lb['health_state'][ifname]['if_addr'] = if_addr + return True + + return False + +def nftables_update(lb): + # Atomically reload nftables table from template + if not os.path.exists(nftables_wlb_conf): + lb['first_install'] = True + elif 'first_install' in lb: + del lb['first_install'] + + render(nftables_wlb_conf, 'load-balancing/nftables-wlb.j2', lb) + + rc, out = rc_cmd(f'nft -f {nftables_wlb_conf}') + + if rc != 0: + print('ERROR: Failed to apply WLB nftables config') + print('Output:', out) + return False + + return True + +def cleanup(lb): + if 'interface_health' in lb: + index = 1 + for ifname, health_conf in lb['interface_health'].items(): + table_num = lb['mark_offset'] + index + run(f'ip route del table {table_num} default') + run(f'ip rule del fwmark {hex(table_num)} table {table_num}') + index += 1 + + run(f'nft delete table ip vyos_wanloadbalance') + +def get_config(): + conf = Config() + base = ['load-balancing', 'wan'] + lb = conf.get_config_dict(base, key_mangling=('-', '_'), + get_first_key=True, with_recursive_defaults=True) + + lb['test_defaults'] = get_defaults(base + ['interface-health', 'A', 'test', 'B'], get_first_key=True) + + return lb + +if __name__ == '__main__': + while commit_in_progress(): + print("Notice: Waiting for commit to complete...") + time.sleep(1) + + lb = get_config() + + lb['health_state'] = {} + lb['mark_offset'] = 0xc8 + + # Create state dicts, interface address and nexthop, install routes and ip rules + if 'interface_health' in lb: + index = 1 + for ifname, health_conf in lb['interface_health'].items(): + table_num = lb['mark_offset'] + index + addr = get_ipv4_address(ifname) + lb['health_state'][ifname] = { + 'if_addr': addr, + 'failure_count': 0, + 'success_count': 0, + 'last_success': 0, + 'last_failure': 0, + 'state': addr is not None, + 'state_changed': False, + 'table_number': table_num, + 'mark': hex(table_num) + } + + if health_conf['nexthop'] == 'dhcp': + lb['health_state'][ifname]['dhcp_nexthop'] = None + + dynamic_nexthop_update(lb, ifname) + else: + run(f'ip route replace table {table_num} default dev {ifname} via {health_conf["nexthop"]}') + + run(f'ip rule add fwmark {hex(table_num)} table {table_num}') + + index += 1 + + nftables_update(lb) + + run('ip route flush cache') + + if 'flush_connections' in lb: + run('conntrack --delete') + run('conntrack -F expect') + + with open(wlb_status_file, 'w') as f: + f.write(json.dumps(lb['health_state'])) + + # Signal handler SIGUSR2 -> dhcpcd update + def handle_sigusr2(signum, frame): + for ifname, health_conf in lb['interface_health'].items(): + if 'nexthop' in health_conf and health_conf['nexthop'] == 'dhcp': + retval = dynamic_nexthop_update(lb, ifname) + + if retval: + nftables_update(lb) + + # Signal handler SIGTERM -> exit + def handle_sigterm(signum, frame): + if os.path.exists(wlb_status_file): + os.unlink(wlb_status_file) + + if os.path.exists(wlb_pid_file): + os.unlink(wlb_pid_file) + + if os.path.exists(nftables_wlb_conf): + os.unlink(nftables_wlb_conf) + + cleanup(lb) + sys.exit(0) + + signal.signal(signal.SIGUSR2, handle_sigusr2) + signal.signal(signal.SIGINT, handle_sigterm) + signal.signal(signal.SIGTERM, handle_sigterm) + + with open(wlb_pid_file, 'w') as f: + f.write(str(os.getpid())) + + # Main loop + + try: + while True: + ip_change = False + + if 'interface_health' in lb: + for ifname, health_conf in lb['interface_health'].items(): + state = lb['health_state'][ifname] + + result = health_check(ifname, health_conf, state=state, test_defaults=lb['test_defaults']) + + state_changed = result != state['state'] + state['state_changed'] = False + + if result: + state['failure_count'] = 0 + state['success_count'] += 1 + state['last_success'] = time.time() + if state_changed and state['success_count'] >= int(health_conf['success_count']): + state['state'] = True + state['state_changed'] = True + elif not result: + state['failure_count'] += 1 + state['success_count'] = 0 + state['last_failure'] = time.time() + if state_changed and state['failure_count'] >= int(health_conf['failure_count']): + state['state'] = False + state['state_changed'] = True + + if state['state_changed']: + state['if_addr'] = get_ipv4_address(ifname) + on_state_change(lb, ifname, state['state']) + + if dynamic_nexthop_update(lb, ifname): + ip_change = True + + if any(state['state_changed'] for ifname, state in lb['health_state'].items()): + if not nftables_update(lb): + break + + run('ip route flush cache') + + if 'flush_connections' in lb: + run('conntrack --delete') + run('conntrack -F expect') + + with open(wlb_status_file, 'w') as f: + f.write(json.dumps(lb['health_state'])) + elif ip_change: + nftables_update(lb) + + time.sleep(sleep_interval) + except Exception as e: + print('WLB ERROR:', e) + + if os.path.exists(wlb_status_file): + os.unlink(wlb_status_file) + + if os.path.exists(wlb_pid_file): + os.unlink(wlb_pid_file) + + if os.path.exists(nftables_wlb_conf): + os.unlink(nftables_wlb_conf) + + cleanup(lb) diff --git a/src/migration-scripts/bgp/5-to-6 b/src/migration-scripts/bgp/5-to-6 new file mode 100644 index 000000000..e6fea6574 --- /dev/null +++ b/src/migration-scripts/bgp/5-to-6 @@ -0,0 +1,39 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T7163: migrate "address-family ipv4|6-unicast redistribute table" from a multi +# leafNode to a tagNode. This is needed to support per table definition of a +# route-map and/or metric + +from vyos.configtree import ConfigTree + +def migrate(config: ConfigTree) -> None: + bgp_base = ['protocols', 'bgp'] + if not config.exists(bgp_base): + return + + for address_family in ['ipv4-unicast', 'ipv6-unicast']: + # there is no non-main routing table beeing redistributed under this addres family + # bail out early and continue with next AFI + table_path = bgp_base + ['address-family', address_family, 'redistribute', 'table'] + if not config.exists(table_path): + continue + + tables = config.return_values(table_path) + config.delete(table_path) + + for table in tables: + config.set(table_path + [table]) + config.set_tag(table_path) diff --git a/src/migration-scripts/lldp/2-to-3 b/src/migration-scripts/lldp/2-to-3 new file mode 100644 index 000000000..93090756c --- /dev/null +++ b/src/migration-scripts/lldp/2-to-3 @@ -0,0 +1,31 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T7165: Migrate LLDP interface disable to 'mode disable' + +from vyos.configtree import ConfigTree + +base = ['service', 'lldp'] + +def migrate(config: ConfigTree) -> None: + interface_base = base + ['interface'] + if not config.exists(interface_base): + # Nothing to do + return + + for interface in config.list_nodes(interface_base): + if config.exists(interface_base + [interface, 'disable']): + config.delete(interface_base + [interface, 'disable']) + config.set(interface_base + [interface, 'mode'], value='disable') diff --git a/src/migration-scripts/policy/8-to-9 b/src/migration-scripts/policy/8-to-9 new file mode 100644 index 000000000..355e48e00 --- /dev/null +++ b/src/migration-scripts/policy/8-to-9 @@ -0,0 +1,49 @@ +# Copyright (C) 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +# T7116: Remove unsupported "internet" community following FRR removal +# From + # set policy route-map <name> rule <ord> set community [add | replace] internet + # set policy community-list <name> rule <ord> regex internet +# To + # set policy route-map <name> rule <ord> set community [add | replace] 0:0 + # set policy community-list <name> rule <ord> regex _0:0_ + +# NOTE: In FRR expanded community-lists, without the '_' delimiters, a regex of +# "0:0" will match "65000:0" as well as "0:0". This doesn't line up with what +# we want when replacing "internet". + +from vyos.configtree import ConfigTree + +rm_base = ['policy', 'route-map'] +cl_base = ['policy', 'community-list'] + +def migrate(config: ConfigTree) -> None: + if config.exists(rm_base): + for policy_name in config.list_nodes(rm_base): + for rule_ord in config.list_nodes(rm_base + [policy_name, 'rule'], path_must_exist=False): + tmp_path = rm_base + [policy_name, 'rule', rule_ord, 'set', 'community'] + if config.exists(tmp_path + ['add']) and config.return_value(tmp_path + ['add']) == 'internet': + config.set(tmp_path + ['add'], '0:0') + if config.exists(tmp_path + ['replace']) and config.return_value(tmp_path + ['replace']) == 'internet': + config.set(tmp_path + ['replace'], '0:0') + + if config.exists(cl_base): + for policy_name in config.list_nodes(cl_base): + for rule_ord in config.list_nodes(cl_base + [policy_name, 'rule'], path_must_exist=False): + tmp_path = cl_base + [policy_name, 'rule', rule_ord, 'regex'] + if config.exists(tmp_path) and config.return_value(tmp_path) == 'internet': + config.set(tmp_path, '_0:0_') + diff --git a/src/migration-scripts/wanloadbalance/3-to-4 b/src/migration-scripts/wanloadbalance/3-to-4 new file mode 100644 index 000000000..e49f46a5b --- /dev/null +++ b/src/migration-scripts/wanloadbalance/3-to-4 @@ -0,0 +1,33 @@ +# Copyright 2025 VyOS maintainers and contributors <maintainers@vyos.io> +# +# This library is free software; you can redistribute it and/or +# modify it under the terms of the GNU Lesser General Public +# License as published by the Free Software Foundation; either +# version 2.1 of the License, or (at your option) any later version. +# +# This library is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# Lesser General Public License for more details. +# +# You should have received a copy of the GNU Lesser General Public License +# along with this library. If not, see <http://www.gnu.org/licenses/>. + +from vyos.configtree import ConfigTree + +base = ['load-balancing', 'wan'] + +def migrate(config: ConfigTree) -> None: + if not config.exists(base): + # Nothing to do + return + + if config.exists(base + ['rule']): + for rule in config.list_nodes(base + ['rule']): + rule_base = base + ['rule', rule] + + if config.exists(rule_base + ['inbound-interface']): + ifname = config.return_value(rule_base + ['inbound-interface']) + + if ifname.endswith('+'): + config.set(rule_base + ['inbound-interface'], value=ifname.replace('+', '*')) diff --git a/src/op_mode/load-balancing_wan.py b/src/op_mode/load-balancing_wan.py new file mode 100755 index 000000000..9fa473802 --- /dev/null +++ b/src/op_mode/load-balancing_wan.py @@ -0,0 +1,117 @@ +#!/usr/bin/env python3 +# +# Copyright (C) 2024 VyOS maintainers and contributors +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License version 2 or later as +# published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +import json +import re +import sys + +from datetime import datetime + +from vyos.config import Config +from vyos.utils.process import cmd + +import vyos.opmode + +wlb_status_file = '/run/wlb_status.json' + +status_format = '''Interface: {ifname} +Status: {status} +Last Status Change: {last_change} +Last Interface Success: {last_success} +Last Interface Failure: {last_failure} +Interface Failures: {failures} +''' + +def _verify(func): + """Decorator checks if WLB config exists""" + from functools import wraps + + @wraps(func) + def _wrapper(*args, **kwargs): + config = Config() + if not config.exists(['load-balancing', 'wan']): + unconf_message = 'WAN load-balancing is not configured' + raise vyos.opmode.UnconfiguredSubsystem(unconf_message) + return func(*args, **kwargs) + return _wrapper + +def _get_raw_data(): + with open(wlb_status_file, 'r') as f: + data = json.loads(f.read()) + if not data: + return {} + return data + +def _get_formatted_output(raw_data): + for ifname, if_data in raw_data.items(): + latest_change = if_data['last_success'] if if_data['last_success'] > if_data['last_failure'] else if_data['last_failure'] + + change_dt = datetime.fromtimestamp(latest_change) if latest_change > 0 else None + success_dt = datetime.fromtimestamp(if_data['last_success']) if if_data['last_success'] > 0 else None + failure_dt = datetime.fromtimestamp(if_data['last_failure']) if if_data['last_failure'] > 0 else None + now = datetime.utcnow() + + fmt_data = { + 'ifname': ifname, + 'status': "active" if if_data['state'] else "failed", + 'last_change': change_dt.strftime("%Y-%m-%d %H:%M:%S") if change_dt else 'N/A', + 'last_success': str(now - success_dt) if success_dt else 'N/A', + 'last_failure': str(now - failure_dt) if failure_dt else 'N/A', + 'failures': if_data['failure_count'] + } + print(status_format.format(**fmt_data)) + +@_verify +def show_summary(raw: bool): + data = _get_raw_data() + + if raw: + return data + else: + return _get_formatted_output(data) + +@_verify +def show_connection(raw: bool): + res = cmd('sudo conntrack -L -n') + lines = res.split("\n") + filtered_lines = [line for line in lines if re.search(r' mark=[1-9]', line)] + + if raw: + return filtered_lines + + for line in lines: + print(line) + +@_verify +def show_status(raw: bool): + res = cmd('sudo nft list chain ip vyos_wanloadbalance wlb_mangle_prerouting') + lines = res.split("\n") + filtered_lines = [line.replace("\t", "") for line in lines[3:-2] if 'meta mark set' not in line] + + if raw: + return filtered_lines + + for line in filtered_lines: + print(line) + +if __name__ == "__main__": + try: + res = vyos.opmode.run(sys.modules[__name__]) + if res: + print(res) + except (ValueError, vyos.opmode.Error) as e: + print(e) + sys.exit(1) diff --git a/src/op_mode/restart.py b/src/op_mode/restart.py index 3b0031f34..efa835485 100755 --- a/src/op_mode/restart.py +++ b/src/op_mode/restart.py @@ -53,6 +53,10 @@ service_map = { 'systemd_service': 'strongswan', 'path': ['vpn', 'ipsec'], }, + 'load-balancing_wan': { + 'systemd_service': 'vyos-wan-load-balance', + 'path': ['load-balancing', 'wan'], + }, 'mdns_repeater': { 'systemd_service': 'avahi-daemon', 'path': ['service', 'mdns', 'repeater'], @@ -86,6 +90,7 @@ services = typing.Literal[ 'haproxy', 'igmp_proxy', 'ipsec', + 'load-balancing_wan', 'mdns_repeater', 'router_advert', 'snmp', diff --git a/src/services/vyos-domain-resolver b/src/services/vyos-domain-resolver index bfc8caa0a..48c6b86d8 100755 --- a/src/services/vyos-domain-resolver +++ b/src/services/vyos-domain-resolver @@ -65,13 +65,15 @@ def get_config(conf, node): node_config = dict_merge(default_values, node_config) - global timeout, cache + if node == base_firewall and 'global_options' in node_config: + global_config = node_config['global_options'] + global timeout, cache - if 'resolver_interval' in node_config: - timeout = int(node_config['resolver_interval']) + if 'resolver_interval' in global_config: + timeout = int(global_config['resolver_interval']) - if 'resolver_cache' in node_config: - cache = True + if 'resolver_cache' in global_config: + cache = True fqdn_config_parse(node_config, node[0]) diff --git a/src/systemd/vyos-wan-load-balance.service b/src/systemd/vyos-wan-load-balance.service index 7d62a2ff6..a59f2c3ae 100644 --- a/src/systemd/vyos-wan-load-balance.service +++ b/src/systemd/vyos-wan-load-balance.service @@ -1,15 +1,11 @@ [Unit] -Description=VyOS WAN load-balancing service +Description=VyOS WAN Load Balancer After=vyos-router.service [Service] -ExecStart=/opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid -ExecReload=/bin/kill -s SIGTERM $MAINPID && sleep 5 && /opt/vyatta/sbin/wan_lb -f /run/load-balance/wlb.conf -d -i /var/run/vyatta/wlb.pid -ExecStop=/bin/kill -s SIGTERM $MAINPID -PIDFile=/var/run/vyatta/wlb.pid -KillMode=process -Restart=on-failure -RestartSec=5s +Type=simple +Restart=always +ExecStart=/usr/bin/python3 /usr/libexec/vyos/vyos-load-balancer.py [Install] WantedBy=multi-user.target |