From 70b7818f75f55de14214b85a12b8ec13d98ff89b Mon Sep 17 00:00:00 2001 From: Matthew Kobayashi Date: Thu, 2 Oct 2025 13:15:03 +1000 Subject: T3680: protocols: add dhclient hooks for dhcp-interface static routes --- src/conf_mode/protocols_static.py | 19 +++++++++ .../98-vyos-static-routes-dhclient-hook | 42 +++++++++++++++++++ .../dhcp/dhclient-exit-hooks.d/97-run-user-hooks | 5 +++ .../dhcp/dhclient-exit-hooks.d/98-run-user-hooks | 5 --- .../98-vyos-static-routes-dhclient-hook | 48 ++++++++++++++++++++++ 5 files changed, 114 insertions(+), 5 deletions(-) create mode 100755 src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook create mode 100755 src/etc/dhcp/dhclient-exit-hooks.d/97-run-user-hooks delete mode 100755 src/etc/dhcp/dhclient-exit-hooks.d/98-run-user-hooks create mode 100755 src/etc/dhcp/dhclient-exit-hooks.d/98-vyos-static-routes-dhclient-hook (limited to 'src') diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 7c730ca81..1db8aa770 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -17,6 +17,7 @@ from ipaddress import IPv4Network from sys import exit from sys import argv +import os from vyos.config import Config from vyos.configverify import has_frr_protocol_in_dict @@ -25,12 +26,14 @@ from vyos.configverify import verify_vrf from vyos.frrender import FRRender from vyos.frrender import get_frrender_dict from vyos.utils.process import is_systemd_service_running +from vyos.utils.file import write_file from vyos.template import render from vyos import ConfigError from vyos import airbag airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' +DHCP_HOOK_IFLIST = '/tmp/static_dhcp_interfaces' def get_config(config=None): if config: @@ -92,6 +95,22 @@ def generate(config_dict): # eqivalent of the C foo ? 'a' : 'b' statement static = vrf and config_dict['vrf']['name'][vrf]['protocols']['static'] or config_dict['static'] + # Collect interfaces that have DHCP configuration for DHCP hooks + dhcp_interfaces = set() + + # Check for DHCP interfaces in route configurations + if 'route' in static: + for prefix, prefix_options in static['route'].items(): + if 'dhcp_interface' in prefix_options: + for interface_name in prefix_options['dhcp_interface']: + dhcp_interfaces.add(interface_name) + + # Write the interface list for DHCP hooks or clean up if empty + if dhcp_interfaces: + write_file(DHCP_HOOK_IFLIST, " ".join(dhcp_interfaces)) + elif os.path.exists(DHCP_HOOK_IFLIST): + os.unlink(DHCP_HOOK_IFLIST) + # Put routing table names in /etc/iproute2/rt_tables render(config_file, 'iproute2/static.conf.j2', static) diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook new file mode 100755 index 000000000..f735b4b29 --- /dev/null +++ b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook @@ -0,0 +1,42 @@ +#!/bin/bash +# +# Copyright 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 . + +DHCP_HOOK_IFLIST="/tmp/static_dhcp_interfaces" + +# Only run if there are static routes with dhcp-interface configured +if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then + return 0 +fi + +# Handle interface state changes that require static route regeneration +# - PREINIT: interface is about to be configured, cleanup old routes +# - EXPIRE: lease has expired, remove routes +# - FAIL: DHCP failed, remove routes +# - RELEASE: lease released, remove routes +# - STOP: dhclient stopped, remove routes +if [ "$reason" == "PREINIT" ] || [ "$reason" == "EXPIRE" ] || [ "$reason" == "FAIL" ] || [ "$reason" == "RELEASE" ] || [ "$reason" == "STOP" ]; then + # Best effort wait for any active commit to finish + sudo python3 - < +# +# 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 . + +DHCP_HOOK_IFLIST="/tmp/static_dhcp_interfaces" + +# Only run if there are static routes with dhcp-interface configured +if ! { [ -f $DHCP_HOOK_IFLIST ] && grep -qw $interface $DHCP_HOOK_IFLIST; }; then + return 0 +fi + +# Re-generate the config on the following events: +# - BOUND: always re-generate +# - RENEW: re-generate if the IP address changed +# - REBIND: re-generate if the IP address changed +# - EXPIRE: always re-generate (route should be removed) +# - RELEASE: always re-generate (route should be removed) +if [ "$reason" == "RENEW" ] || [ "$reason" == "REBIND" ]; then + if [ "$old_routers" == "$new_routers" ]; then + return 0 + fi +elif [ "$reason" != "BOUND" ] && [ "$reason" != "EXPIRE" ] && [ "$reason" != "RELEASE" ]; then + return 0 +fi + +# Best effort wait for any active commit to finish +sudo python3 - < Date: Fri, 17 Oct 2025 13:58:01 +0300 Subject: T5811: Make static dhcp-interface routes robust Solves the problem that vyos-configs in FRRender caches configuation and DHCP changes are ignored. * Add src/helpers/vyos-request-configd-update.py that requests vyos-configd to update FRR configuration. * Make dhclient hooks use it instead of calling protocols_static.py * Make FRRender cache not only configuration but also DHCP gateways so that is any of them changes, FRR configuration is updated --- python/vyos/defaults.py | 2 ++ python/vyos/frrender.py | 15 ++++++++++- src/conf_mode/protocols_static.py | 3 ++- .../98-vyos-static-routes-dhclient-hook | 11 +------- .../98-vyos-static-routes-dhclient-hook | 13 ++------- src/helpers/vyos-request-configd-update.py | 31 ++++++++++++++++++++++ src/services/vyos-configd | 3 ++- 7 files changed, 54 insertions(+), 24 deletions(-) create mode 100755 src/helpers/vyos-request-configd-update.py (limited to 'src') diff --git a/python/vyos/defaults.py b/python/vyos/defaults.py index 43cde38e9..25d7fd50c 100644 --- a/python/vyos/defaults.py +++ b/python/vyos/defaults.py @@ -61,6 +61,8 @@ config_files = { config_status = '/tmp/vyos-config-status' api_config_state = '/run/http-api-state' frr_debug_enable = '/tmp/vyos.frr.debug' +static_route_dhcp_interfaces_path = '/tmp/static_dhcp_interfaces' +vyos_configd_socket_path = 'ipc:///run/vyos-configd.sock' cfg_group = 'vyattacfg' diff --git a/python/vyos/frrender.py b/python/vyos/frrender.py index decec8aa2..91fd3667d 100644 --- a/python/vyos/frrender.py +++ b/python/vyos/frrender.py @@ -31,11 +31,14 @@ from vyos.config import config_dict_merge from vyos.configdict import get_dhcp_interfaces from vyos.configdict import get_pppoe_interfaces from vyos.defaults import frr_debug_enable +from vyos.defaults import static_route_dhcp_interfaces_path from vyos.utils.dict import dict_search from vyos.utils.dict import dict_set_nested +from vyos.utils.file import read_file from vyos.utils.file import write_file from vyos.utils.process import cmd from vyos.utils.process import rc_cmd +from vyos.template import get_dhcp_router from vyos.template import render_to_string from vyos import ConfigError @@ -634,6 +637,7 @@ def get_frrender_dict(conf: Config, argv=None) -> dict: class FRRender: cached_config_dict = {} + cached_dhcp_gateways = {} def __init__(self): self._frr_conf = '/run/frr/config/vyos.frr.conf' @@ -646,11 +650,20 @@ class FRRender: tmp = type(config_dict) raise ValueError(f'Config must be of type "dict" and not "{tmp}"!') + dhcp_gateways = { + interface: get_dhcp_router(interface) + for interface in read_file(static_route_dhcp_interfaces_path, '').split() + } - if self.cached_config_dict == config_dict: + if ( + self.cached_config_dict == config_dict + and self.cached_dhcp_gateways == dhcp_gateways + ): debug('FRR: NO CHANGES DETECTED') return False + self.cached_config_dict = config_dict + self.cached_dhcp_gateways = dhcp_gateways def inline_helper(config_dict) -> str: output = '!\n' diff --git a/src/conf_mode/protocols_static.py b/src/conf_mode/protocols_static.py index 1db8aa770..6310be13d 100755 --- a/src/conf_mode/protocols_static.py +++ b/src/conf_mode/protocols_static.py @@ -30,10 +30,11 @@ from vyos.utils.file import write_file from vyos.template import render from vyos import ConfigError from vyos import airbag +from vyos import defaults airbag.enable() config_file = '/etc/iproute2/rt_tables.d/vyos-static.conf' -DHCP_HOOK_IFLIST = '/tmp/static_dhcp_interfaces' +DHCP_HOOK_IFLIST = defaults.static_route_dhcp_interfaces_path def get_config(config=None): if config: diff --git a/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook index f735b4b29..439a8dd87 100755 --- a/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook +++ b/src/etc/dhcp/dhclient-enter-hooks.d/98-vyos-static-routes-dhclient-hook @@ -28,15 +28,6 @@ fi # - RELEASE: lease released, remove routes # - STOP: dhclient stopped, remove routes if [ "$reason" == "PREINIT" ] || [ "$reason" == "EXPIRE" ] || [ "$reason" == "FAIL" ] || [ "$reason" == "RELEASE" ] || [ "$reason" == "STOP" ]; then - # Best effort wait for any active commit to finish - sudo python3 - <