From 782e400d6f276a8a4ee51c92f02e01e7695f55fb Mon Sep 17 00:00:00 2001 From: Christian Breunig Date: Fri, 4 Aug 2023 20:13:29 +0200 Subject: dhcp: T5428: client renewal fails when running inside VRF vyos@vyos# run show vrf MGMT processes 2282 sshd There is no dhclient process running in given VRF. dhclient complains it can not send out packets via the given interface (as it's not bound to that VRF) Aug 02 20:29:54 dhclient[1686]: send_packet: Network is unreachable Aug 02 20:29:54 dhclient[1686]: send_packet: please consult README file regarding broadcast address. Aug 02 20:29:54 dhclient[1686]: dhclient.c:3001: Failed to send 300 byte long packet over fallback interface. --- data/templates/dhcp-client/daemon-options.j2 | 4 --- data/templates/dhcp-client/override.conf.j2 | 15 ++++++++ python/vyos/configdict.py | 4 +++ python/vyos/ifconfig/interface.py | 52 +++++++++++++++------------- src/systemd/dhclient@.service | 7 +--- 5 files changed, 48 insertions(+), 34 deletions(-) delete mode 100644 data/templates/dhcp-client/daemon-options.j2 create mode 100644 data/templates/dhcp-client/override.conf.j2 diff --git a/data/templates/dhcp-client/daemon-options.j2 b/data/templates/dhcp-client/daemon-options.j2 deleted file mode 100644 index b21ad08ab..000000000 --- a/data/templates/dhcp-client/daemon-options.j2 +++ /dev/null @@ -1,4 +0,0 @@ -### Autogenerated by interface.py ### -{% set if_metric = '-e IF_METRIC=' ~ dhcp_options.default_route_distance if dhcp_options.default_route_distance is vyos_defined else '' %} -DHCLIENT_OPTS="-nw -cf /var/lib/dhcp/dhclient_{{ ifname }}.conf -pf /var/lib/dhcp/dhclient_{{ ifname }}.pid -lf /var/lib/dhcp/dhclient_{{ ifname }}.leases {{ if_metric }} {{ ifname }}" - diff --git a/data/templates/dhcp-client/override.conf.j2 b/data/templates/dhcp-client/override.conf.j2 new file mode 100644 index 000000000..fcb23cb52 --- /dev/null +++ b/data/templates/dhcp-client/override.conf.j2 @@ -0,0 +1,15 @@ +### Autogenerated by interface.py ### +{% set vrf_command = 'ip vrf exec ' ~ vrf ~ ' ' if vrf is vyos_defined else '' %} +{% set if_metric = '-e IF_METRIC=' ~ dhcp_options.default_route_distance if dhcp_options.default_route_distance is vyos_defined else '' %} +{% set dhclient_options = '-d -nw -cf /var/lib/dhcp/dhclient_' ~ ifname ~ '.conf -pf /var/lib/dhcp/dhclient_' ~ ifname ~ '.pid -lf /var/lib/dhcp/dhclient_' ~ ifname ~ '.leases ' ~ if_metric %} + +[Unit] +ConditionPathExists={{ isc_dhclient_dir }}/dhclient_%i.conf + +[Service] +ExecStart= +ExecStart={{ vrf_command }}/sbin/dhclient -4 {{ dhclient_options }} {{ ifname }} +ExecStop= +ExecStop={{ vrf_command }}/sbin/dhclient -4 {{ dhclient_options }} {{ ifname }} -r +WorkingDirectory={{ isc_dhclient_dir }} +PIDFile={{ isc_dhclient_dir }}/dhclient_%i.pid diff --git a/python/vyos/configdict.py b/python/vyos/configdict.py index f642d38f2..fd7b1b997 100644 --- a/python/vyos/configdict.py +++ b/python/vyos/configdict.py @@ -455,6 +455,10 @@ def get_interface_dict(config, base, ifname='', recursive_defaults=True): dhcp = is_node_changed(config, base + [ifname, 'dhcp-options']) if dhcp: dict.update({'dhcp_options_changed' : {}}) + # Changine interface VRF assignemnts require a DHCP restart, too + dhcp = is_node_changed(config, base + [ifname, 'vrf']) + if dhcp: dict.update({'dhcp_options_changed' : {}}) + # Some interfaces come with a source_interface which must also not be part # of any other bond or bridge interface as it is exclusivly assigned as the # Kernels "lower" interface to this new "virtual/upper" interface. diff --git a/python/vyos/ifconfig/interface.py b/python/vyos/ifconfig/interface.py index fe8752d24..c2787136d 100644 --- a/python/vyos/ifconfig/interface.py +++ b/python/vyos/ifconfig/interface.py @@ -1242,40 +1242,44 @@ class Interface(Control): ifname = self.ifname config_base = directories['isc_dhclient_dir'] + '/dhclient' - config_file = f'{config_base}_{ifname}.conf' - options_file = f'{config_base}_{ifname}.options' - pid_file = f'{config_base}_{ifname}.pid' - lease_file = f'{config_base}_{ifname}.leases' + dhclient_config_file = f'{config_base}_{ifname}.conf' + dhclient_lease_file = f'{config_base}_{ifname}.leases' + systemd_override_file = f'/run/systemd/system/dhclient@{ifname}.service.d/10-override.conf' systemd_service = f'dhclient@{ifname}.service' + self.config['isc_dhclient_dir'] = directories['isc_dhclient_dir'] + # 'up' check is mandatory b/c even if the interface is A/D, as soon as # the DHCP client is started the interface will be placed in u/u state. # This is not what we intended to do when disabling an interface. - if enable and 'disable' not in self._config: - if dict_search('dhcp_options.host_name', self._config) == None: + if enable and 'disable' not in self.config: + if dict_search('dhcp_options.host_name', self.config) == None: # read configured system hostname. # maybe change to vyos hostd client ??? hostname = 'vyos' with open('/etc/hostname', 'r') as f: hostname = f.read().rstrip('\n') tmp = {'dhcp_options' : { 'host_name' : hostname}} - self._config = dict_merge(tmp, self._config) + self.config = dict_merge(tmp, self.config) + + render(systemd_override_file, 'dhcp-client/override.conf.j2', self.config) + render(dhclient_config_file, 'dhcp-client/ipv4.j2', self.config) - render(options_file, 'dhcp-client/daemon-options.j2', self._config) - render(config_file, 'dhcp-client/ipv4.j2', self._config) + # Reload systemd unit definitons as some options are dynamically generated + self._cmd('systemctl daemon-reload') # When the DHCP client is restarted a brief outage will occur, as # the old lease is released a new one is acquired (T4203). We will # only restart DHCP client if it's option changed, or if it's not # running, but it should be running (e.g. on system startup) - if 'dhcp_options_changed' in self._config or not is_systemd_service_active(systemd_service): + if 'dhcp_options_changed' in self.config or not is_systemd_service_active(systemd_service): return self._cmd(f'systemctl restart {systemd_service}') return None else: if is_systemd_service_active(systemd_service): self._cmd(f'systemctl stop {systemd_service}') # cleanup old config files - for file in [config_file, options_file, pid_file, lease_file]: + for file in [dhclient_config_file, systemd_override_file, dhclient_lease_file]: if os.path.isfile(file): os.remove(file) @@ -1292,9 +1296,9 @@ class Interface(Control): options_file = f'/run/dhcp6c/dhcp6c.{ifname}.options' systemd_service = f'dhcp6c@{ifname}.service' - if enable and 'disable' not in self._config: - render(options_file, 'dhcp-client/dhcp6c_daemon-options.j2', self._config) - render(config_file, 'dhcp-client/ipv6.j2', self._config) + if enable and 'disable' not in self.config: + render(options_file, 'dhcp-client/dhcp6c_daemon-options.j2', self.config) + render(config_file, 'dhcp-client/ipv6.j2', self.config) # We must ignore any return codes. This is required to enable # DHCPv6-PD for interfaces which are yet not up and running. @@ -1311,20 +1315,20 @@ class Interface(Control): # - https://man7.org/linux/man-pages/man8/tc-mirred.8.html # Depening if we are the source or the target interface of the port # mirror we need to setup some variables. - source_if = self._config['ifname'] + source_if = self.config['ifname'] mirror_config = None - if 'mirror' in self._config: - mirror_config = self._config['mirror'] - if 'is_mirror_intf' in self._config: - source_if = next(iter(self._config['is_mirror_intf'])) - mirror_config = self._config['is_mirror_intf'][source_if].get('mirror', None) + if 'mirror' in self.config: + mirror_config = self.config['mirror'] + if 'is_mirror_intf' in self.config: + source_if = next(iter(self.config['is_mirror_intf'])) + mirror_config = self.config['is_mirror_intf'][source_if].get('mirror', None) redirect_config = None # clear existing ingess - ignore errors (e.g. "Error: Cannot find specified # qdisc on specified device") - we simply cleanup all stuff here - if not 'traffic_policy' in self._config: + if not 'traffic_policy' in self.config: self._popen(f'tc qdisc del dev {source_if} parent ffff: 2>/dev/null'); self._popen(f'tc qdisc del dev {source_if} parent 1: 2>/dev/null'); @@ -1348,11 +1352,11 @@ class Interface(Control): if err: print('tc qdisc(filter for mirror port failed') # Apply interface traffic redirection policy - elif 'redirect' in self._config: + elif 'redirect' in self.config: _, err = self._popen(f'tc qdisc add dev {source_if} handle ffff: ingress') if err: print(f'tc qdisc add for redirect failed!') - target_if = self._config['redirect'] + target_if = self.config['redirect'] _, err = self._popen(f'tc filter add dev {source_if} parent ffff: protocol '\ f'all prio 10 u32 match u32 0 0 flowid 1:1 action mirred '\ f'egress redirect dev {target_if}') @@ -1371,7 +1375,7 @@ class Interface(Control): # Cache the configuration - it will be reused inside e.g. DHCP handler # XXX: maybe pass the option via __init__ in the future and rename this # method to apply()? - self._config = config + self.config = config # Change interface MAC address - re-set to real hardware address (hw-id) # if custom mac is removed. Skip if bond member. diff --git a/src/systemd/dhclient@.service b/src/systemd/dhclient@.service index 4711a0d70..6d41de232 100644 --- a/src/systemd/dhclient@.service +++ b/src/systemd/dhclient@.service @@ -1,18 +1,13 @@ [Unit] Description=DHCP client on %i Documentation=man:dhclient(8) -ConditionPathExists=/var/lib/dhcp/dhclient_%i.conf -ConditionPathExists=/var/lib/dhcp/dhclient_%i.options StartLimitIntervalSec=0 After=vyos-router.service [Service] -WorkingDirectory=/var/lib/dhcp Type=exec -EnvironmentFile=-/var/lib/dhcp/dhclient_%i.options -PIDFile=/var/lib/dhcp/dhclient_%i.pid ExecStart=/sbin/dhclient -4 -d $DHCLIENT_OPTS -ExecStop=/sbin/dhclient -4 $DHCLIENT_OPTS -r +ExecStop=/sbin/dhclient -4 -d $DHCLIENT_OPTS -r Restart=always RestartPreventExitStatus= RestartSec=10 -- cgit v1.2.3