From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from firstgate.proxmox.com (firstgate.proxmox.com [IPv6:2a01:7e0:0:424::9]) by lore.proxmox.com (Postfix) with ESMTPS id 252E41FF139 for ; Tue, 10 Feb 2026 11:51:56 +0100 (CET) Received: from firstgate.proxmox.com (localhost [127.0.0.1]) by firstgate.proxmox.com (Proxmox) with ESMTP id 8536D1C71C; Tue, 10 Feb 2026 11:52:38 +0100 (CET) Content-Type: text/plain; charset=UTF-8 Date: Tue, 10 Feb 2026 11:51:56 +0100 Message-Id: Subject: Re: [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client From: "Christoph Heiss" To: "Proxmox VE development discussion" Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Mailer: aerc 0.21.0 References: <20251212113220.516467-1-c.heiss@proxmox.com> In-Reply-To: <20251212113220.516467-1-c.heiss@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1770720632021 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.051 Adjusted score from AWL reputation of From: address BAYES_00 -1.9 Bayes spam probability is 0 to 1% DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment RCVD_IN_VALIDITY_CERTIFIED_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_RPBL_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. RCVD_IN_VALIDITY_SAFE_BLOCKED 0.001 ADMINISTRATOR NOTICE: The query to Validity was blocked. See https://knowledge.validity.com/hc/en-us/articles/20961730681243 for more information. SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: GFD47ZBRG43QHK2FAN3RPFBKASSVECOF X-Message-ID-Hash: GFD47ZBRG43QHK2FAN3RPFBKASSVECOF X-MailFrom: c.heiss@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Ping, still applies. On Fri Dec 12, 2025 at 12:30 PM CET, Christoph Heiss wrote: > This adds support for dhcpcd(8) as a second dhcp client. dhclient has > been deprecated by upstream. > > With this patch, ifupdown2 will prefer dhcpcd, falling back to dhclient > if it cannot find the former for backwards compatibility. > > Upstream-PR: https://github.com/CumulusNetworks/ifupdown2/pull/347 > Signed-off-by: Christoph Heiss > --- > To test, first `dhcpcd` must be installed using `apt install dhcpcd`. > > Then, put > > auto nic1 > iface nic1 inet dhcp > iface nic8 inet6 dhcp > > in /etc/network/interfaces (renaming `nic1` as needed), and commenting > in/out the respective `inet` or `inet6` line and running > `ifreload -avd; ip a s nic1` to check the results. > > debian/patches/series | 1 + > ...-support-for-dhcpcd-8-as-dhcp-client.patch | 1009 +++++++++++++++++ > 2 files changed, 1010 insertions(+) > create mode 100644 debian/patches/upstream/0003-addons-dhcp-add-support-= for-dhcpcd-8-as-dhcp-client.patch > > diff --git a/debian/patches/series b/debian/patches/series > index 2865533..ea7ec89 100644 > --- a/debian/patches/series > +++ b/debian/patches/series > @@ -16,3 +16,4 @@ upstream/0001-use-raw-strings-for-regex-to-fix-backslas= h-interpret.patch > upstream/0002-vxlan-add-support-for-IPv6-vxlan-local-tunnelip.patch > pve/0014-nlmanager-read-ipv6-devconf-disable_ipv6-attribute-t.patch > pve/0015-revert-addons-bond-warn-if-sub-interface-is-detected-on-bond-sl= ave.patch > +upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-as-dhcp-client.patch > diff --git a/debian/patches/upstream/0003-addons-dhcp-add-support-for-dhc= pcd-8-as-dhcp-client.patch b/debian/patches/upstream/0003-addons-dhcp-add-s= upport-for-dhcpcd-8-as-dhcp-client.patch > new file mode 100644 > index 0000000..fc9e9cd > --- /dev/null > +++ b/debian/patches/upstream/0003-addons-dhcp-add-support-for-dhcpcd-8-a= s-dhcp-client.patch > @@ -0,0 +1,1009 @@ > +From c96ed60826714873ad06315bdfa9ec6028fd2f08 Mon Sep 17 00:00:00 2001 > +From: Christoph Heiss > +Date: Fri, 12 Dec 2025 12:05:51 +0100 > +Subject: [PATCH] addons: dhcp: add support for dhcpcd(8) as dhcp client > + > +This adds support for dhcpcd(8) as a second dhcp client. dhclient has > +been deprecated by upstream. > + > +With this patch, ifupdown2 will prefer dhcpcd, falling back to dhclient > +if it cannot find the former for backwards compatibility. > + > +Signed-off-by: Christoph Heiss > +--- > + docs/source/addonshelperapiref.rst | 18 +++ > + ifupdown2/addons/address.py | 41 ++++--- > + ifupdown2/addons/dhcp.py | 155 +++++++++++++++--------- > + ifupdown2/addons/vrf.py | 34 +++--- > + ifupdown2/ifupdown/exceptions.py | 4 + > + ifupdown2/ifupdown/utils.py | 18 ++- > + ifupdown2/ifupdownaddons/dhclient.py | 91 +++++++------- > + ifupdown2/ifupdownaddons/dhcp_client.py | 49 ++++++++ > + ifupdown2/ifupdownaddons/dhcpcd.py | 147 ++++++++++++++++++++++ > + ifupdown2/lib/sysfs.py | 7 ++ > + 10 files changed, 425 insertions(+), 139 deletions(-) > + create mode 100644 ifupdown2/ifupdownaddons/dhcp_client.py > + create mode 100644 ifupdown2/ifupdownaddons/dhcpcd.py > + > +diff --git a/docs/source/addonshelperapiref.rst b/docs/source/addonshelp= erapiref.rst > +index 6eaa0ef..1dd3396 100644 > +--- a/docs/source/addonshelperapiref.rst > ++++ b/docs/source/addonshelperapiref.rst > +@@ -22,3 +22,21 @@ Helper module to interact with dhclient tools. > + .. automodule:: dhclient > + > + .. autoclass:: dhclient > ++ > ++dhcpcd > ++=3D=3D=3D=3D=3D=3D > ++ > ++Helper module to interact with the dhcpcd(8) DHCP client. > ++ > ++.. automodule:: dhcpcd > ++ > ++.. autoclass:: dhcpcd > ++ > ++DhcpClient > ++=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D > ++ > ++Helper module to interact with the dhcpcd(8) DHCP client. > ++ > ++.. automodule:: dhcp_client > ++ > ++.. autoclass:: DhcpClient > +diff --git a/ifupdown2/addons/address.py b/ifupdown2/addons/address.py > +index 46226a9..8ae69ee 100644 > +--- a/ifupdown2/addons/address.py > ++++ b/ifupdown2/addons/address.py > +@@ -17,8 +17,9 @@ try: > + > + from ifupdown2.ifupdown.iface import ifaceType, ifaceLinkKind, ifac= eLinkPrivFlags, ifaceStatus, iface > + from ifupdown2.ifupdown.utils import utils > ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable > + > +- from ifupdown2.ifupdownaddons.dhclient import dhclient > ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient > + from ifupdown2.ifupdownaddons.modulebase import moduleBase > + > + import ifupdown2.nlmanager.ipnetwork as ipnetwork > +@@ -33,8 +34,9 @@ except (ImportError, ModuleNotFoundError): > + > + from ifupdown.iface import ifaceType, ifaceLinkKind, ifaceLinkPrivF= lags, ifaceStatus, iface > + from ifupdown.utils import utils > ++ from ifupdown.exceptions import NoDhcpClientAvailable > + > +- from ifupdownaddons.dhclient import dhclient > ++ from ifupdownaddons.dhcp_client import DhcpClient > + from ifupdownaddons.modulebase import moduleBase > + > + import nlmanager.ipnetwork as ipnetwork > +@@ -211,6 +213,8 @@ class address(AddonWithIpBlackList, moduleBase): > + > + DEFAULT_MTU_STRING =3D "1500" > + > ++ dhcpcmd: DhcpClient | None > ++ > + def __init__(self, *args, **kargs): > + AddonWithIpBlackList.__init__(self) > + moduleBase.__init__(self, *args, **kargs) > +@@ -291,6 +295,11 @@ class address(AddonWithIpBlackList, moduleBase): > + except Exception: > + self.default_autoconf =3D 1 > + > ++ try: > ++ self.dhcpcmd =3D DhcpClient() > ++ except NoDhcpClientAvailable as e: > ++ self.dhcpcmd =3D None > ++ > + def __policy_get_default_mtu(self): > + default_mtu =3D policymanager.policymanager_api.get_attr_defaul= t( > + module_name=3Dself.__class__.__name__, > +@@ -1207,18 +1216,17 @@ class address(AddonWithIpBlackList, moduleBase): > + if (addr_method not in ["dhcp", "ppp"] and not ifupdownfla= gs.flags.PERFMODE and > + not (ifaceobj.flags & iface.HAS_SIBLINGS)): > + # if not running in perf mode and ifaceobj does not hav= e > +- # any sibling iface objects, kill any stale dhclient > +- # processes > +- dhclientcmd =3D dhclient() > +- if dhclientcmd.is_running(ifaceobj.name): > +- # release any dhcp leases > +- dhclientcmd.release(ifaceobj.name) > +- self.cache.force_address_flush_family(ifaceobj.name= , socket.AF_INET) > +- force_reapply =3D True > +- elif dhclientcmd.is_running6(ifaceobj.name): > +- dhclientcmd.release6(ifaceobj.name) > +- self.cache.force_address_flush_family(ifaceobj.name= , socket.AF_INET6) > +- force_reapply =3D True > ++ # any sibling iface objects, kill any stale dhcp client= processes > ++ if self.dhcpcmd: > ++ if self.dhcpcmd.is_running(ifaceobj.name): > ++ # release any dhcp leases > ++ self.dhcpcmd.release(ifaceobj.name) > ++ self.cache.force_address_flush_family(ifaceobj.= name, socket.AF_INET) > ++ force_reapply =3D True > ++ elif self.dhcpcmd.is_running6(ifaceobj.name): > ++ self.dhcpcmd.release6(ifaceobj.name) > ++ self.cache.force_address_flush_family(ifaceobj.= name, socket.AF_INET6) > ++ force_reapply =3D True > + except Exception: > + pass > + > +@@ -1624,9 +1632,8 @@ class address(AddonWithIpBlackList, moduleBase): > + > + self.query_running_ipv6_addrgen(ifaceobjrunning) > + > +- dhclientcmd =3D dhclient() > +- if (dhclientcmd.is_running(ifaceobjrunning.name) or > +- dhclientcmd.is_running6(ifaceobjrunning.name)): > ++ if self.dhcpcmd and (self.dhcpcmd.is_running(ifaceobjrunning.na= me) or > ++ self.dhcpcmd.is_running6(ifaceobjrunning.name)): > + # If dhcp is configured on the interface, we skip it > + return > + > +diff --git a/ifupdown2/addons/dhcp.py b/ifupdown2/addons/dhcp.py > +index 22bbdb4..cdad890 100644 > +--- a/ifupdown2/addons/dhcp.py > ++++ b/ifupdown2/addons/dhcp.py > +@@ -18,8 +18,9 @@ try: > + > + from ifupdown2.ifupdown.iface import ifaceLinkPrivFlags, ifaceStatu= s > + from ifupdown2.ifupdown.utils import utils > ++ from ifupdown2.ifupdown.exceptions import moduleNotSupported, NoDhc= pClientAvailable > + > +- from ifupdown2.ifupdownaddons.dhclient import dhclient > ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient > + from ifupdown2.ifupdownaddons.modulebase import moduleBase > + except (ImportError, ModuleNotFoundError): > + from lib.addon import Addon > +@@ -30,8 +31,9 @@ except (ImportError, ModuleNotFoundError): > + > + from ifupdown.iface import ifaceLinkPrivFlags, ifaceStatus > + from ifupdown.utils import utils > ++ from ifupdown.exceptions import moduleNotSupported, NoDhcpClientAva= ilable > + > +- from ifupdownaddons.dhclient import dhclient > ++ from ifupdownaddons.dhcp_client import DhcpClient > + from ifupdownaddons.modulebase import moduleBase > + > + > +@@ -40,13 +42,14 @@ class dhcp(Addon, moduleBase): > + > + # by default we won't perform any dhcp retry > + # this can be changed by setting the module global > +- # policy: dhclient_retry_on_failure > +- DHCLIENT_RETRY_ON_FAILURE =3D 0 > ++ # policy: dhcp_retry_on_failure > ++ DHCP_RETRY_ON_FAILURE =3D 0 > ++ > ++ dhcpcmd: DhcpClient > + > + def __init__(self, *args, **kargs): > + Addon.__init__(self) > + moduleBase.__init__(self, *args, **kargs) > +- self.dhclientcmd =3D dhclient(**kargs) > + vrf_id =3D self._get_vrf_context() > + if vrf_id and vrf_id =3D=3D 'mgmt': > + self.mgmt_vrf_context =3D True > +@@ -55,24 +58,41 @@ class dhcp(Addon, moduleBase): > + self.logger.info('mgmt vrf_context =3D %s' %self.mgmt_vrf_conte= xt) > + > + try: > +- self.dhclient_retry_on_failure =3D int( > +- policymanager.policymanager_api.get_module_globals( > +- module_name=3Dself.__class__.__name__, > +- attr=3D"dhclient_retry_on_failure" > ++ try: > ++ self.dhcp_retry_on_failure =3D int( > ++ policymanager.policymanager_api.get_module_globals( > ++ module_name=3Dself.__class__.__name__, > ++ attr=3D"dhcp_retry_on_failure" > ++ ) > ++ ) > ++ except: > ++ self.dhcp_retry_on_failure =3D int( > ++ policymanager.policymanager_api.get_module_globals( > ++ module_name=3Dself.__class__.__name__, > ++ attr=3D"dhclient_retry_on_failure" > ++ ) > + ) > +- ) > + except Exception: > +- self.dhclient_retry_on_failure =3D self.DHCLIENT_RETRY_ON_F= AILURE > ++ self.dhcp_retry_on_failure =3D self.DHCP_RETRY_ON_FAILURE > + > +- if self.dhclient_retry_on_failure < 0: > +- self.dhclient_retry_on_failure =3D 0 > ++ if self.dhcp_retry_on_failure < 0: > ++ self.dhcp_retry_on_failure =3D 0 > + > +- self.logger.debug("dhclient: dhclient_retry_on_failure set to %= s" % self.dhclient_retry_on_failure) > ++ try: > ++ self.dhcpcmd =3D DhcpClient() > ++ except NoDhcpClientAvailable as e: > ++ self.logger.warn('no dhcp client available') > ++ raise moduleNotSupported(e.message) > ++ > ++ self.logger.debug("dhcp: dhcp_retry_on_failure set to %s" % sel= f.dhcp_retry_on_failure) > + > + def syntax_check(self, ifaceobj, ifaceobj_getfunc): > + return self.is_dhcp_allowed_on(ifaceobj, syntax_check=3DTrue) > + > + def is_dhcp_allowed_on(self, ifaceobj, syntax_check): > ++ if not self.dhcpcmd: > ++ return False > ++ > + if ifaceobj.addr_method and 'dhcp' in ifaceobj.addr_method: > + return utils.is_addr_ip_allowed_on(ifaceobj, syntax_check= =3DTrue) > + return True > +@@ -97,9 +117,9 @@ class dhcp(Addon, moduleBase): > + pass > + return ips > + > +- def dhclient_start_and_check(self, ifname, family, handler, wait=3D= True, **handler_kwargs): > ++ def dhcp_client_start_and_check(self, ifname, family, handler, wait= =3DTrue, **handler_kwargs): > + ip_config_before =3D self.get_current_ip_configured(ifname, fam= ily) > +- retry =3D self.dhclient_retry_on_failure > ++ retry =3D self.dhcp_retry_on_failure > + > + while retry >=3D 0: > + handler(ifname, wait=3Dwait, **handler_kwargs) > +@@ -107,47 +127,52 @@ class dhcp(Addon, moduleBase): > + # In most case, the client won't have the time to find = anything > + # with the wait=3DFalse param. > + return > +- retry =3D self.dhclient_check(ifname, family, ip_config_bef= ore, retry, handler_kwargs.get("cmd_prefix")) > ++ retry =3D self.dhcp_client_check(ifname, family, ip_config_= before, retry, handler_kwargs.get("cmd_prefix")) > + > +- def dhclient_check(self, ifname, family, ip_config_before, retry, d= hclient_cmd_prefix): > ++ def dhcp_client_check(self, ifname, family, ip_config_before, retry= , dhcp_cmd_prefix: list[str]): > + diff =3D self.get_current_ip_configured(ifname, family).differe= nce(ip_config_before) > + > + if diff: > + self.logger.info( > +- "%s: dhclient: new address%s detected: %s" > ++ "%s: dhcp: new address%s detected: %s" > + % (ifname, "es" if len(diff) > 1 else "", ", ".join(dif= f)) > + ) > + return -1 > + else: > + if retry > 0: > + self.logger.error( > +- "%s: dhclient: couldn't detect new ip address, = retrying %s more times..." > ++ "%s: dhcp: couldn't detect new ip address, retr= ying %s more times..." > + % (ifname, retry) > + ) > +- self.dhclientcmd.stop(ifname) > ++ self.dhcpcmd.stop(ifname) > + else: > +- self.logger.error("%s: dhclient: timeout failed to = detect new ip addresses" % ifname) > ++ self.logger.error("%s: dhcp: timeout failed to dete= ct new ip addresses" % ifname) > + return -1 > + retry -=3D 1 > + return retry > + > + def _up(self, ifaceobj): > ++ real_ifname =3D self.cache.link_translate_altname(ifaceobj.name= ) > ++ > + # if dhclient is already running do not stop and start it > +- dhclient4_running =3D self.dhclientcmd.is_running(ifaceobj.name= ) > +- dhclient6_running =3D self.dhclientcmd.is_running6(ifaceobj.nam= e) > ++ dhcp4_running =3D self.dhcpcmd.is_running(real_ifname) > ++ dhcp6_running =3D self.dhcpcmd.is_running6(real_ifname) > ++ > ++ self.logger.debug(f'dhcp v4 client running: {dhcp4_running}') > ++ self.logger.debug(f'dhcp v6 client running: {dhcp6_running}') > + > + # today if we have an interface with both inet and inet6, if we > + # remove the inet or inet6 or both then execute ifreload, we ne= ed > + # to release/kill the appropriate dhclient(4/6) if they are run= ning > +- self._down_stale_dhcp_config(ifaceobj, 'inet', dhclient4_runnin= g) > +- self._down_stale_dhcp_config(ifaceobj, 'inet6', dhclient6_runni= ng) > ++ self._down_stale_dhcp_config(ifaceobj, 'inet', dhcp4_running) > ++ self._down_stale_dhcp_config(ifaceobj, 'inet6', dhcp6_running) > + > + if ifaceobj.link_privflags & ifaceLinkPrivFlags.KEEP_LINK_DOWN: > + self.logger.info("%s: skipping dhcp configuration: link-dow= n yes" % ifaceobj.name) > + return > + > + try: > +- dhclient_cmd_prefix =3D None > ++ dhcp_cmd_prefix =3D [] > + dhcp_wait =3D policymanager.policymanager_api.get_attr_defa= ult( > + module_name=3Dself.__class__.__name__, attr=3D'dhcp-wai= t') > + wait =3D str(dhcp_wait).lower() !=3D "no" > +@@ -163,38 +188,43 @@ class dhcp(Addon, moduleBase): > + vrf =3D ifaceobj.get_attr_value_first('vrf') > + if (vrf and self.vrf_exec_cmd_prefix and > + self.cache.link_exists(vrf)): > +- dhclient_cmd_prefix =3D '%s %s' %(self.vrf_exec_cmd_pre= fix, vrf) > ++ dhcp_cmd_prefix =3D self.vrf_exec_cmd_prefix.split() + = [vrf] > + elif self.mgmt_vrf_context: > +- dhclient_cmd_prefix =3D '%s %s' %(self.vrf_exec_cmd_pre= fix, 'default') > ++ dhcp_cmd_prefix =3D self.vrf_exec_cmd_prefix.split() + = ['default'] > + self.logger.info('detected mgmt vrf context starting dh= client in default vrf context') > + > + if 'inet' in ifaceobj.addr_family: > +- if dhclient4_running: > +- self.logger.info('dhclient4 already running on %s. = ' > +- 'Not restarting.' % ifaceobj.name) > ++ if dhcp4_running: > ++ self.logger.info('dhcp4 client already running on %= s. ' > ++ 'Not restarting.' % real_ifname) > + else: > +- # First release any existing dhclient processes > ++ # First release any existing dhcp processes > + try: > + if not ifupdownflags.flags.PERFMODE: > +- self.dhclientcmd.stop(ifaceobj.name) > ++ self.dhcpcmd.stop(real_ifname) > + except Exception: > + pass > + > +- self.dhclient_start_and_check( > +- ifaceobj.name, > ++ self.dhcp_client_start_and_check( > ++ real_ifname, > + "inet", > +- self.dhclientcmd.start, > ++ self.dhcpcmd.start, > + wait=3Dwait, > +- cmd_prefix=3Ddhclient_cmd_prefix > ++ cmd_prefix=3Ddhcp_cmd_prefix > + ) > ++ elif dhcp4_running: > ++ # release and stop the running dhcp client if the ipv4 = dhcp config vanished > ++ self.logger.debug('dhcp4 running but config vanished, s= topping') > ++ self.dhcpcmd.release(real_ifname) > ++ self.dhcpcmd.stop(real_ifname) > + > + if 'inet6' in ifaceobj.addr_family: > +- if dhclient6_running: > +- self.logger.info('dhclient6 already running on %s. = ' > ++ if dhcp6_running: > ++ self.logger.info('dhcp6 client already running on %= s. ' > + 'Not restarting.' % ifaceobj.name) > + else: > + try: > +- self.dhclientcmd.stop6(ifaceobj.name, duid=3Ddh= cp6_duid) > ++ self.dhcpcmd.stop6(real_ifname, duid=3Ddhcp6_du= id) > + except Exception: > + pass > + #add delay before starting IPv6 dhclient to > +@@ -202,17 +232,21 @@ class dhcp(Addon, moduleBase): > + if timeout > 1: > + time.sleep(1) > + while timeout: > +- addr_output =3D utils.exec_command('%s -6 addr = show %s' > +- %(utils.ip_cmd= , ifaceobj.name)) > +- r =3D re.search('inet6 .* scope link', addr_out= put) > +- if r: > +- self.dhclientcmd.start6(ifaceobj.name, > +- wait=3Dwait, > +- cmd_prefix=3Ddhclie= nt_cmd_prefix, duid=3Ddhcp6_duid) > +- return > ++ if self.cache.link_is_up(real_ifname): > ++ break > + timeout -=3D 1 > + if timeout: > + time.sleep(1) > ++ > ++ self.dhcpcmd.start6(real_ifname, > ++ wait=3Dwait, > ++ cmd_prefix=3Ddhcp_cmd_prefi= x, duid=3Ddhcp6_duid) > ++ elif dhcp6_running: > ++ # release and stop the running dhcp client if the ipv6 = dhcp config vanished > ++ self.logger.debug('dhcp6 running but config vanished, s= topping') > ++ self.dhcpcmd.release6(real_ifname) > ++ self.dhcpcmd.stop6(real_ifname) > ++ > + except Exception as e: > + self.logger.error("%s: %s" % (ifaceobj.name, str(e))) > + ifaceobj.set_status(ifaceStatus.ERROR) > +@@ -229,18 +263,20 @@ class dhcp(Addon, moduleBase): > + ifaceobj.addr_family =3D addr_family > + > + def _dhcp_down(self, ifaceobj): > +- dhclient_cmd_prefix =3D None > ++ dhcp_cmd_prefix =3D [] > ++ ifname =3D self.cache.link_translate_altname(ifaceobj.name) > ++ > + vrf =3D ifaceobj.get_attr_value_first('vrf') > + if (vrf and self.vrf_exec_cmd_prefix and > + self.cache.link_exists(vrf)): > +- dhclient_cmd_prefix =3D '%s %s' %(self.vrf_exec_cmd_prefix,= vrf) > ++ dhcp_cmd_prefix =3D self.vrf_exec_cmd_prefix.split() + [vrf= ] > + dhcp6_duid =3D policymanager.policymanager_api.get_iface_defaul= t(module_name=3Dself.__class__.__name__, \ > + = ifname=3Difaceobj.name, attr=3D'dhcp6-duid') > + if 'inet6' in ifaceobj.addr_family: > +- self.dhclientcmd.release6(ifaceobj.name, dhclient_cmd_prefi= x, duid=3Ddhcp6_duid) > ++ self.dhcpcmd.release6(ifname, dhcp_cmd_prefix, duid=3Ddhcp6= _duid) > + self.cache.force_address_flush_family(ifaceobj.name, socket= .AF_INET6) > + if 'inet' in ifaceobj.addr_family: > +- self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix= ) > ++ self.dhcpcmd.release(ifname, dhcp_cmd_prefix) > + self.cache.force_address_flush_family(ifaceobj.name, socket= .AF_INET) > + > + def _down(self, ifaceobj): > +@@ -250,9 +286,10 @@ class dhcp(Addon, moduleBase): > + def _query_check(self, ifaceobj, ifaceobjcurr): > + status =3D ifaceStatus.SUCCESS > + dhcp_running =3D False > ++ ifname =3D self.cache.link_translate_altname(ifaceobjcurr.name) > + > +- dhcp_v4 =3D self.dhclientcmd.is_running(ifaceobjcurr.name) > +- dhcp_v6 =3D self.dhclientcmd.is_running6(ifaceobjcurr.name) > ++ dhcp_v4 =3D self.dhcpcmd.is_running(ifname) > ++ dhcp_v6 =3D self.dhcpcmd.is_running6(ifname) > + > + if dhcp_v4: > + dhcp_running =3D True > +@@ -271,12 +308,14 @@ class dhcp(Addon, moduleBase): > + ifaceobjcurr.status =3D status > + > + def _query_running(self, ifaceobjrunning): > +- if not self.cache.link_exists(ifaceobjrunning.name): > ++ ifname =3D self.cache.link_translate_altname(ifaceobjrunning.na= me) > ++ > ++ if not self.cache.link_exists(ifname): > + return > +- if self.dhclientcmd.is_running(ifaceobjrunning.name): > ++ if self.dhcpcmd.is_running(ifname): > + ifaceobjrunning.addr_family.append('inet') > + ifaceobjrunning.addr_method =3D 'dhcp' > +- if self.dhclientcmd.is_running6(ifaceobjrunning.name): > ++ if self.dhcpcmd.is_running6(ifname): > + ifaceobjrunning.addr_family.append('inet6') > + ifaceobjrunning.addr_method =3D 'dhcp6' > + > +diff --git a/ifupdown2/addons/vrf.py b/ifupdown2/addons/vrf.py > +index 2c92a12..04fec56 100644 > +--- a/ifupdown2/addons/vrf.py > ++++ b/ifupdown2/addons/vrf.py > +@@ -19,10 +19,11 @@ try: > + > + from ifupdown2.ifupdown.iface import ifaceRole, ifaceLinkKind, ifac= eLinkPrivFlags, ifaceLinkType > + from ifupdown2.ifupdown.utils import utils > ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable > + > + from ifupdown2.nlmanager.nlmanager import Link > + > +- from ifupdown2.ifupdownaddons.dhclient import dhclient > ++ from ifupdown2.ifupdownaddons.dhcp_client import DhcpClient > + from ifupdown2.ifupdownaddons.utilsbase import * > + from ifupdown2.ifupdownaddons.modulebase import moduleBase > + except (ImportError, ModuleNotFoundError): > +@@ -34,14 +35,14 @@ except (ImportError, ModuleNotFoundError): > + > + from ifupdown.iface import ifaceRole, ifaceLinkKind, ifaceLinkPrivF= lags, ifaceLinkType > + from ifupdown.utils import utils > ++ from ifupdown.exceptions import NoDhcpClientAvailable > + > + from nlmanager.nlmanager import Link > + > +- from ifupdownaddons.dhclient import dhclient > ++ from ifupdownaddons.dhcp_client import DhcpClient > + from ifupdownaddons.utilsbase import * > + from ifupdownaddons.modulebase import moduleBase > + > +- > + class vrfPrivFlags: > + PROCESSED =3D 0x1 > + > +@@ -80,10 +81,11 @@ class vrf(Addon, moduleBase): > + "0": "unspec" > + } > + > ++ dhcpcmd: DhcpClient | None > ++ > + def __init__(self, *args, **kargs): > + Addon.__init__(self) > + moduleBase.__init__(self, *args, **kargs) > +- self.dhclientcmd =3D None > + self.name =3D self.__class__.__name__ > + self.vrf_mgmt_devname =3D policymanager.policymanager_api.get_m= odule_globals( > + module_name=3Dself.__class__.__name__, > +@@ -128,6 +130,11 @@ class vrf(Addon, moduleBase): > + self.ip6_rule_cache =3D [] > + self.logger.warning('vrf: cache v6: %s' % str(e)) > + > ++ try: > ++ self.dhcpcmd =3D DhcpClient() > ++ except NoDhcpClientAvailable as e: > ++ self.dhcpcmd =3D None > ++ > + #self.logger.debug("vrf: ip rule cache") > + #self.logger.info(self.ip_rule_cache) > + > +@@ -403,7 +410,7 @@ class vrf(Addon, moduleBase): > + def _up_vrf_slave_without_master(self, ifacename, vrfname, ifaceobj= , vrf_master_objs, ifaceobj_getfunc=3DNone): > + """ If we have a vrf slave that has dhcp configured, bring up t= he > + vrf master now. This is needed because vrf has special hand= ling > +- in dhclient hook which requires the vrf master to be presen= t """ > ++ in dhcp hook which requires the vrf master to be present ""= " > + vrf_master =3D None > + if len(ifaceobj.upperifaces) > 1 and ifaceobj_getfunc: > + for upper_iface in ifaceobj.upperifaces: > +@@ -460,14 +467,15 @@ class vrf(Addon, moduleBase): > + > + def _down_dhcp_slave(self, ifaceobj, vrfname): > + try: > +- dhclient_cmd_prefix =3D None > ++ dhcp_cmd_prefix =3D None > + if (vrfname and self.vrf_exec_cmd_prefix and > + self.cache.link_exists(vrfname)): > +- dhclient_cmd_prefix =3D '%s %s' %(self.vrf_exec_cmd_pre= fix, > +- vrfname) > +- self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix= ) > ++ dhcp_cmd_prefix =3D self.vrf_exec_cmd_prefix.split() + = [vrfname] > ++ > ++ ifacename =3D self.cache.link_translate_altname(ifaceobj.na= me) > ++ self.dhcpcmd.release(ifacename, dhcp_cmd_prefix) > + except Exception: > +- # ignore any dhclient release errors > ++ # ignore any dhcp client release errors > + pass > + > + def _handle_existing_connections(self, ifaceobj, vrfname): > +@@ -1127,10 +1135,6 @@ class vrf(Addon, moduleBase): > + """ returns list of ops supported by this module """ > + return list(self._run_ops.keys()) > + > +- def _init_command_handlers(self): > +- if not self.dhclientcmd: > +- self.dhclientcmd =3D dhclient() > +- > + def run(self, ifaceobj, operation, query_ifaceobj=3DNone, > + ifaceobj_getfunc=3DNone, **extra_args): > + """ run bond configuration on the interface object passed as ar= gument > +@@ -1152,7 +1156,7 @@ class vrf(Addon, moduleBase): > + op_handler =3D self._run_ops.get(operation) > + if not op_handler: > + return > +- self._init_command_handlers() > ++ > + if operation =3D=3D 'query-checkcurr': > + op_handler(self, ifaceobj, query_ifaceobj) > + else: > +diff --git a/ifupdown2/ifupdown/exceptions.py b/ifupdown2/ifupdown/excep= tions.py > +index 0dd16a6..54deacd 100644 > +--- a/ifupdown2/ifupdown/exceptions.py > ++++ b/ifupdown2/ifupdown/exceptions.py > +@@ -61,3 +61,7 @@ class moduleNotSupported(Error): > + > + class ReservedVlanException(Error): > + pass > ++ > ++ > ++class NoDhcpClientAvailable(Exception): > ++ pass > +diff --git a/ifupdown2/ifupdown/utils.py b/ifupdown2/ifupdown/utils.py > +index 9b7ec9b..bc7617b 100644 > +--- a/ifupdown2/ifupdown/utils.py > ++++ b/ifupdown2/ifupdown/utils.py > +@@ -107,6 +107,7 @@ class utils(): > + ethtool_cmd =3D '/sbin/ethtool' > + systemctl_cmd =3D '/bin/systemctl' > + dpkg_cmd =3D '/usr/bin/dpkg' > ++ dhcpcd_cmd =3D '/usr/sbin/dhcpcd' > + > + logger.info("utils init command paths") > + for cmd in ['bridge', > +@@ -123,7 +124,8 @@ class utils(): > + 'mstpctl', > + 'ethtool', > + 'systemctl', > +- 'dpkg' > ++ 'dpkg', > ++ 'dhcpcd', > + ]: > + if os.path.exists(vars()[cmd + '_cmd']): > + continue > +@@ -575,4 +577,18 @@ class utils(): > + raise > + return vnid > + > ++ @staticmethod > ++ def pid_exists(pid: int) -> bool: > ++ """ > ++ Check whether there is a process with the given PID. > ++ """ > ++ try: > ++ # > If sig is 0, then no signal is sent, but existence and = permission checks are still > ++ # > performed; this can be used to check for the existence = of a process ID or process > ++ # > group ID that the caller is permitted to signal. > ++ os.kill(pid, 0) > ++ except OSError: > ++ return False > ++ return True > ++ > + fcntl.fcntl(utils.DEVNULL, fcntl.F_SETFD, fcntl.FD_CLOEXEC) > +diff --git a/ifupdown2/ifupdownaddons/dhclient.py b/ifupdown2/ifupdownad= dons/dhclient.py > +index c10db65..bfb0208 100644 > +--- a/ifupdown2/ifupdownaddons/dhclient.py > ++++ b/ifupdown2/ifupdownaddons/dhclient.py > +@@ -10,49 +10,40 @@ import errno > + try: > + from ifupdown2.ifupdown.utils import utils > + from ifupdown2.ifupdownaddons.utilsbase import * > ++ from ifupdown2.lib.sysfs import Sysfs > + except (ImportError, ModuleNotFoundError): > + from ifupdown.utils import utils > + from ifupdownaddons.utilsbase import * > +- > ++ from lib.sysfs import Sysfs > + > + class dhclient(utilsBase): > + """ This class contains helper methods to interact with the dhclien= t > + utility """ > + > +- def _pid_exists(self, pidfilename): > +- if os.path.exists(pidfilename): > +- try: > +- return os.readlink( > +- "/proc/%s/exe" % self.read_file_oneline(pidfilename= ) > +- ).endswith("dhclient") > +- except OSError as e: > +- try: > +- if e.errno =3D=3D errno.EACCES: > +- return os.path.exists("/proc/%s" % self.read_fi= le_oneline(pidfilename)) > +- except Exception: > +- return False > +- except Exception: > +- return False > +- return False > ++ MAX_RETRIES =3D 5 > + > +- def is_running(self, ifacename): > +- return self._pid_exists('/run/dhclient.%s.pid' %ifacename) > ++ def __init__(self, *args, **kwargs): > ++ super().__init__(*args, **kwargs) > ++ if not os.path.exists('/sbin/dhclient3') and not os.path.exists= ('/sbin/dhclient'): > ++ raise RuntimeError(f'missing required executable: /sbin/dhc= lient3 or /sbin/dhclient') > + > +- def is_running6(self, ifacename): > +- return self._pid_exists('/run/dhclient6.%s.pid' %ifacename) > ++ def is_running(self, ifacename: str) -> bool: > ++ pid =3D self.read_file_oneline(f'/run/dhclient.{ifacename}.pid'= ) > ++ try: > ++ return utils.pid_exists(int(pid)) > ++ except (TypeError, ValueError): > ++ return False > + > +- def _run_dhclient_cmd(self, cmd, cmd_prefix=3DNone): > +- if not cmd_prefix: > +- cmd_aslist =3D [] > +- else: > +- cmd_aslist =3D cmd_prefix.split() > +- if cmd_aslist: > +- cmd_aslist.extend(cmd) > +- else: > +- cmd_aslist =3D cmd > +- utils.exec_commandl(cmd_aslist, stdout=3DNone, stderr=3DNone) > ++ def is_running6(self, ifacename: str) -> bool: > ++ pid =3D self.read_file_oneline(f'/run/dhclient6.{ifacename}.pid= ') > ++ try: > ++ return utils.pid_exists(int(pid)) > ++ except (TypeError, ValueError): > ++ return False > ++ > ++ def stop(self, ifacename: str, cmd_prefix: list[str] =3D []): > ++ self.logger.debug(f'stopping dhclient on {ifacename}') > + > +- def stop(self, ifacename, cmd_prefix=3DNone): > + if os.path.exists('/sbin/dhclient3'): > + cmd =3D ['/sbin/dhclient3', '-x', '-pf', > + '/run/dhclient.%s.pid' %ifacename, '-lf', > +@@ -63,18 +54,14 @@ class dhclient(utilsBase): > + '/run/dhclient.%s.pid' %ifacename, > + '-lf', '/var/lib/dhcp/dhclient.%s.leases' %ifacename= , > + '%s' %ifacename] > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > + > +- def start(self, ifacename, wait=3DTrue, cmd_prefix=3DNone): > ++ def start(self, ifacename: str, wait: bool =3D True, cmd_prefix: li= st[str] =3D []): > ++ self.logger.debug(f'starting dhclient on {ifacename}') > + retries =3D 0 > +- out =3D "0" > + > + # wait if interface isn't up yet > +- while '1' not in out and retries < 5: > +- path =3D '/sys/class/net/%s/carrier' %ifacename > +- out =3D self.read_file_oneline(path) > +- if out is None: > +- break # No sysfs file found for this iface > ++ while not Sysfs.link_has_carrier(ifacename) and retries < self.= MAX_RETRIES: > + retries +=3D 1 > + time.sleep(1) > + > +@@ -90,9 +77,11 @@ class dhclient(utilsBase): > + '%s' %ifacename] > + if not wait: > + cmd.append('-nw') > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > ++ > ++ def release(self, ifacename: str, cmd_prefix: list[str] =3D []): > ++ self.logger.debug(f'releasing lease on {ifacename}') > + > +- def release(self, ifacename, cmd_prefix=3DNone): > + if os.path.exists('/sbin/dhclient3'): > + cmd =3D ['/sbin/dhclient3', '-r', '-pf', > + '/run/dhclient.%s.pid' %ifacename, '-lf', > +@@ -103,9 +92,11 @@ class dhclient(utilsBase): > + '/run/dhclient.%s.pid' %ifacename, > + '-lf', '/var/lib/dhcp/dhclient.%s.leases' %ifacename= , > + '%s' %ifacename] > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > ++ > ++ def start6(self, ifacename: str, wait: bool =3D True, cmd_prefix: l= ist[str] =3D [], duid: str | None =3D None): > ++ self.logger.debug(f'starting v6 dhclient on {ifacename}') > + > +- def start6(self, ifacename, wait=3DTrue, cmd_prefix=3DNone, duid=3D= None): > + cmd =3D ['/sbin/dhclient', '-6', '-pf', > + '/run/dhclient6.%s.pid' %ifacename, '-lf', > + '/var/lib/dhcp/dhclient6.%s.leases' % ifacename, > +@@ -115,9 +106,11 @@ class dhclient(utilsBase): > + if duid is not None: > + cmd.append('-D') > + cmd.append(duid) > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > ++ > ++ def stop6(self, ifacename: str, cmd_prefix: list[str] =3D [], duid:= str | None =3D None): > ++ self.logger.debug(f'stopping v6 dhclient on {ifacename}') > + > +- def stop6(self, ifacename, cmd_prefix=3DNone, duid=3DNone): > + cmd =3D ['/sbin/dhclient', '-6', '-x', '-pf', > + '/run/dhclient6.%s.pid' % ifacename, '-lf', > + '/var/lib/dhcp/dhclient6.%s.leases' % ifacename, > +@@ -125,9 +118,11 @@ class dhclient(utilsBase): > + if duid is not None: > + cmd.append('-D') > + cmd.append(duid) > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > ++ > ++ def release6(self, ifacename: str, cmd_prefix: list[str] =3D [], du= id: str | None =3D None): > ++ self.logger.debug(f'releasing v6 lease on {ifacename}') > + > +- def release6(self, ifacename, cmd_prefix=3DNone, duid=3DNone): > + cmd =3D ['/sbin/dhclient', '-6', '-r', '-pf', > + '/run/dhclient6.%s.pid' %ifacename, > + '-lf', '/var/lib/dhcp/dhclient6.%s.leases' % ifacename, > +@@ -135,4 +130,4 @@ class dhclient(utilsBase): > + if duid is not None: > + cmd.append('-D') > + cmd.append(duid) > +- self._run_dhclient_cmd(cmd, cmd_prefix) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone, stderr=3DN= one) > +diff --git a/ifupdown2/ifupdownaddons/dhcp_client.py b/ifupdown2/ifupdow= naddons/dhcp_client.py > +new file mode 100644 > +index 0000000..934119f > +--- /dev/null > ++++ b/ifupdown2/ifupdownaddons/dhcp_client.py > +@@ -0,0 +1,49 @@ > ++import logging > ++ > ++try: > ++ from ifupdown2.ifupdownaddons.dhclient import dhclient > ++ from ifupdown2.ifupdownaddons.dhcpcd import DhcpcdClient > ++ from ifupdown2.ifupdown.exceptions import NoDhcpClientAvailable > ++except (ImportError, ModuleNotFoundError): > ++ from ifupdownaddons.dhclient import dhclient > ++ from ifupdownaddons.dhcpcd import DhcpcdClient > ++ from ifupdown.exceptions import NoDhcpClientAvailable > ++ > ++ > ++class DhcpClient: > ++ """ > ++ Automatically selects an available DHCP client (preferring dhcpcd o= ver dhclient) > ++ and forwards all method calls to the respective DHCP client. > ++ """ > ++ > ++ impl: DhcpcdClient | dhclient > ++ > ++ _PROXIED_METHODS =3D [ > ++ 'is_running', > ++ 'is_running6', > ++ 'start', > ++ 'start6', > ++ 'stop', > ++ 'stop6', > ++ 'release', > ++ 'release6', > ++ ] > ++ > ++ def __init__(self, *args, **kwargs): > ++ self.logger =3D logging.getLogger('ifupdown.dhcp_client') > ++ > ++ try: > ++ self.impl =3D DhcpcdClient(**kwargs) > ++ self.logger.info('using dhcpcd client') > ++ except RuntimeError: > ++ self.logger.debug('dhcpcd client unavailable, trying deprec= ated dhclient') > ++ try: > ++ self.impl =3D dhclient(**kwargs) > ++ self.logger.info('using deprecated dhclient client') > ++ except RuntimeError: > ++ raise NoDhcpClientAvailable('neither dhcpcd nor dhclien= t executable found') > ++ > ++ def __getattr__(self, name): > ++ if name in self._PROXIED_METHODS: > ++ return getattr(self.impl, name) > ++ raise AttributeError > +diff --git a/ifupdown2/ifupdownaddons/dhcpcd.py b/ifupdown2/ifupdownaddo= ns/dhcpcd.py > +new file mode 100644 > +index 0000000..fa4eb87 > +--- /dev/null > ++++ b/ifupdown2/ifupdownaddons/dhcpcd.py > +@@ -0,0 +1,147 @@ > ++#!/usr/bin/env python3 > ++ > ++import os > ++import time > ++ > ++try: > ++ from ifupdown2.ifupdown.utils import utils > ++ from ifupdown2.ifupdownaddons.utilsbase import * > ++ from ifupdown2.lib.sysfs import Sysfs > ++except (ImportError, ModuleNotFoundError): > ++ from ifupdown.utils import utils > ++ from ifupdownaddons.utilsbase import * > ++ from lib.sysfs import Sysfs > ++ > ++class DhcpcdClient(utilsBase): > ++ """ > ++ This class contains helper methods to interact with the dhcpcd(8) c= lient > ++ """ > ++ MAX_RETRIES =3D 5 > ++ > ++ def __init__(self, *args, **kwargs): > ++ super().__init__(*args, **kwargs) > ++ if not os.path.exists(utils.dhcpcd_cmd): > ++ raise RuntimeError(f'missing required executable: {utils.dh= cpcd_cmd}') > ++ > ++ def _start(self, cmd_prefix: list[str], ifacename: str, wait: bool,= ipv6: bool, duid: str | None =3D None): > ++ """ > ++ Starts the dhcpcd(8) with the given arguments. > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ :param ifacename: Interface name > ++ :param wait: Whether to wait until a lease has been acquired be= fore dhcpcd becomes a daemon. > ++ :param ipv6: Whether to request a IPv6 address, otherwise IPv4. > ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT' > ++ """ > ++ retries =3D 0 > ++ > ++ # wait if interface isn't up yet > ++ while not Sysfs.link_has_carrier(ifacename) and retries < self.= MAX_RETRIES: > ++ retries +=3D 1 > ++ time.sleep(1) > ++ > ++ cmd =3D [utils.dhcpcd_cmd] > ++ > ++ if ipv6: > ++ cmd.append('--ipv6only') > ++ else: > ++ cmd.append('--ipv4only') > ++ > ++ if wait: > ++ cmd.append('--waitip') > ++ > ++ if duid: > ++ cmd.extend(['--duid', duid]) > ++ > ++ cmd.append(ifacename) > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone) > ++ > ++ def is_running(self, ifacename: str) -> bool: > ++ """ > ++ Checks whether an IPv4 dhcpcd(8) daemon is running for the give= n interface. > ++ :param ifacename: Interface name > ++ """ > ++ pid =3D self.read_file_oneline(f'/run/dhcpcd/{ifacename}-4.pid'= ) > ++ try: > ++ return utils.pid_exists(int(pid)) > ++ except (TypeError, ValueError): > ++ return False > ++ > ++ def is_running6(self, ifacename: str) -> bool: > ++ """ > ++ Checks whether an IPv6 dhcpcd(8) daemon is running for the give= n interface. > ++ :param ifacename: Interface name > ++ """ > ++ pid =3D self.read_file_oneline(f'/run/dhcpcd/{ifacename}-6.pid'= ) > ++ try: > ++ return utils.pid_exists(int(pid)) > ++ except (TypeError, ValueError): > ++ return False > ++ > ++ def start(self, ifacename: str, wait: bool =3D True, cmd_prefix: li= st[str] =3D []): > ++ """ > ++ Starts the dhcpcd(8) for leasing an IPv4 address. > ++ :param ifacename: Interface name > ++ :param wait: Whether to wait until a lease has been acquired be= fore dhcpcd becomes a daemon. > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ """ > ++ self.logger.debug(f'starting dhcpcd client on {ifacename}') > ++ self._start(cmd_prefix, ifacename, wait, ipv6=3DFalse) > ++ > ++ def stop(self, ifacename: str, cmd_prefix: list[str] =3D []): > ++ """ > ++ Stops the IPv4 dhcpcd daemon for the given interface. > ++ Does not release the current lease. > ++ :param ifacename: Interface name > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ """ > ++ self.logger.debug(f'stopping dhcpcd client on {ifacename}') > ++ > ++ cmd =3D [utils.dhcpcd_cmd, '--ipv4only', '--exit', ifacename] > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone) > ++ > ++ def release(self, ifacename: str, cmd_prefix: list[str] =3D []): > ++ """ > ++ Releases the current IPv4 lease for the given interface. > ++ :param ifacename: Interface name > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ """ > ++ self.logger.debug(f'releasing lease on {ifacename}') > ++ > ++ cmd =3D [utils.dhcpcd_cmd, '--ipv4only', '--release', ifacename= ] > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone) > ++ > ++ def start6(self, ifacename: str, wait: bool =3D True, cmd_prefix: l= ist[str] =3D [], duid: str | None =3D None): > ++ """ > ++ Starts the dhcpcd(8) for leasing an IPv6 address. > ++ :param ifacename: Interface name > ++ :param wait: Whether to wait until a lease has been acquired be= fore dhcpcd becomes a daemon. > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT' > ++ """ > ++ self.logger.debug(f'starting v6 dhcpcd client on {ifacename}') > ++ self._start(cmd_prefix, ifacename, wait, True, duid) > ++ > ++ def stop6(self, ifacename: str, cmd_prefix: list[str] =3D [], duid:= str | None =3D None): > ++ """ > ++ Stops the IPv6 dhcpcd daemon for the given interface. > ++ Does not release the current lease. > ++ :param ifacename: Interface name > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT' > ++ """ > ++ self.logger.debug(f'stopping v6 dhcpcd client on {ifacename}') > ++ > ++ cmd =3D [utils.dhcpcd_cmd, '--ipv6only', '--exit', ifacename] > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone) > ++ > ++ def release6(self, ifacename: str, cmd_prefix: list[str] =3D [], du= id: str | None =3D None): > ++ """ > ++ Releases the current IPv4 lease for the given interface. > ++ :param ifacename: Interface name > ++ :param cmd_prefix: Optional list of strings to prefix the dhcpc= d command with > ++ :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT' > ++ """ > ++ self.logger.debug(f'releasing v6 lease on {ifacename}') > ++ > ++ cmd =3D [utils.dhcpcd_cmd, '--ipv6only', '--release', ifacename= ] > ++ utils.exec_commandl(cmd_prefix + cmd, stdout=3DNone) > +diff --git a/ifupdown2/lib/sysfs.py b/ifupdown2/lib/sysfs.py > +index 6aa4284..d3b998b 100644 > +--- a/ifupdown2/lib/sysfs.py > ++++ b/ifupdown2/lib/sysfs.py > +@@ -100,6 +100,13 @@ class __Sysfs(IO, Requirements): > + """ > + return "up" =3D=3D self.read_file_oneline("/sys/class/net/%s/op= erstate" % ifname) > + > ++ def link_has_carrier(self, name: str) -> bool: > ++ """ > ++ Checks whether the given interface has CARRIER set > ++ """ > ++ out =3D self.read_file_oneline(f'/sys/class/net/{name}/carrier'= ) > ++ return out is not None and '1' in out > ++ > + def get_link_address(self, ifname): > + """ > + Read MAC hardware address from sysfs > +-- > +2.51.2 > +