public inbox for pve-devel@lists.proxmox.com
 help / color / mirror / Atom feed
* [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client
@ 2025-12-12 11:30 Christoph Heiss
  2025-12-12 11:30 ` [pve-devel] [PATCH ifupdown2 2/2] gitignore: add build artifacts Christoph Heiss
  2026-02-10 10:51 ` [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss
  0 siblings, 2 replies; 3+ messages in thread
From: Christoph Heiss @ 2025-12-12 11:30 UTC (permalink / raw)
  To: pve-devel

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 <c.heiss@proxmox.com>
---
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-backslash-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-slave.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-dhcpcd-8-as-dhcp-client.patch b/debian/patches/upstream/0003-addons-dhcp-add-support-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-as-dhcp-client.patch
@@ -0,0 +1,1009 @@
+From c96ed60826714873ad06315bdfa9ec6028fd2f08 Mon Sep 17 00:00:00 2001
+From: Christoph Heiss <c.heiss@proxmox.com>
+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 <c.heiss@proxmox.com>
+---
+ 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/addonshelperapiref.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
++======
++
++Helper module to interact with the dhcpcd(8) DHCP client.
++
++.. automodule:: dhcpcd
++
++.. autoclass:: dhcpcd
++
++DhcpClient
++==========
++
++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, ifaceLinkPrivFlags, 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, ifaceLinkPrivFlags, 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 = "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 = 1
+ 
++        try:
++            self.dhcpcmd = DhcpClient()
++        except NoDhcpClientAvailable as e:
++            self.dhcpcmd = None
++
+     def __policy_get_default_mtu(self):
+         default_mtu = policymanager.policymanager_api.get_attr_default(
+             module_name=self.__class__.__name__,
+@@ -1207,18 +1216,17 @@ class address(AddonWithIpBlackList, moduleBase):
+             if (addr_method not in ["dhcp", "ppp"]  and not ifupdownflags.flags.PERFMODE and
+                     not (ifaceobj.flags & iface.HAS_SIBLINGS)):
+                 # if not running in perf mode and ifaceobj does not have
+-                # any sibling iface objects, kill any stale dhclient
+-                # processes
+-                dhclientcmd = 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 = True
+-                elif dhclientcmd.is_running6(ifaceobj.name):
+-                    dhclientcmd.release6(ifaceobj.name)
+-                    self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET6)
+-                    force_reapply = 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 = 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 = True
+         except Exception:
+             pass
+ 
+@@ -1624,9 +1632,8 @@ class address(AddonWithIpBlackList, moduleBase):
+ 
+         self.query_running_ipv6_addrgen(ifaceobjrunning)
+ 
+-        dhclientcmd = dhclient()
+-        if (dhclientcmd.is_running(ifaceobjrunning.name) or
+-                dhclientcmd.is_running6(ifaceobjrunning.name)):
++        if self.dhcpcmd and (self.dhcpcmd.is_running(ifaceobjrunning.name) 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, ifaceStatus
+     from ifupdown2.ifupdown.utils import utils
++    from ifupdown2.ifupdown.exceptions import moduleNotSupported, NoDhcpClientAvailable
+ 
+-    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, NoDhcpClientAvailable
+ 
+-    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 = 0
++    # policy: dhcp_retry_on_failure
++    DHCP_RETRY_ON_FAILURE = 0
++
++    dhcpcmd: DhcpClient
+ 
+     def __init__(self, *args, **kargs):
+         Addon.__init__(self)
+         moduleBase.__init__(self, *args, **kargs)
+-        self.dhclientcmd = dhclient(**kargs)
+         vrf_id = self._get_vrf_context()
+         if vrf_id and vrf_id == 'mgmt':
+             self.mgmt_vrf_context = True
+@@ -55,24 +58,41 @@ class dhcp(Addon, moduleBase):
+         self.logger.info('mgmt vrf_context = %s' %self.mgmt_vrf_context)
+ 
+         try:
+-            self.dhclient_retry_on_failure = int(
+-                policymanager.policymanager_api.get_module_globals(
+-                    module_name=self.__class__.__name__,
+-                    attr="dhclient_retry_on_failure"
++            try:
++                self.dhcp_retry_on_failure = int(
++                    policymanager.policymanager_api.get_module_globals(
++                        module_name=self.__class__.__name__,
++                        attr="dhcp_retry_on_failure"
++                    )
++                )
++            except:
++                self.dhcp_retry_on_failure = int(
++                    policymanager.policymanager_api.get_module_globals(
++                        module_name=self.__class__.__name__,
++                        attr="dhclient_retry_on_failure"
++                    )
+                 )
+-            )
+         except Exception:
+-            self.dhclient_retry_on_failure = self.DHCLIENT_RETRY_ON_FAILURE
++            self.dhcp_retry_on_failure = self.DHCP_RETRY_ON_FAILURE
+ 
+-        if self.dhclient_retry_on_failure < 0:
+-            self.dhclient_retry_on_failure = 0
++        if self.dhcp_retry_on_failure < 0:
++            self.dhcp_retry_on_failure = 0
+ 
+-        self.logger.debug("dhclient: dhclient_retry_on_failure set to %s" % self.dhclient_retry_on_failure)
++        try:
++            self.dhcpcmd = 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" % self.dhcp_retry_on_failure)
+ 
+     def syntax_check(self, ifaceobj, ifaceobj_getfunc):
+         return self.is_dhcp_allowed_on(ifaceobj, syntax_check=True)
+ 
+     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=True)
+         return True
+@@ -97,9 +117,9 @@ class dhcp(Addon, moduleBase):
+             pass
+         return ips
+ 
+-    def dhclient_start_and_check(self, ifname, family, handler, wait=True, **handler_kwargs):
++    def dhcp_client_start_and_check(self, ifname, family, handler, wait=True, **handler_kwargs):
+         ip_config_before = self.get_current_ip_configured(ifname, family)
+-        retry = self.dhclient_retry_on_failure
++        retry = self.dhcp_retry_on_failure
+ 
+         while retry >= 0:
+             handler(ifname, wait=wait, **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=False param.
+                 return
+-            retry = self.dhclient_check(ifname, family, ip_config_before, retry, handler_kwargs.get("cmd_prefix"))
++            retry = 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, dhclient_cmd_prefix):
++    def dhcp_client_check(self, ifname, family, ip_config_before, retry, dhcp_cmd_prefix: list[str]):
+         diff = self.get_current_ip_configured(ifname, family).difference(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(diff))
+             )
+             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, retrying %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 detect new ip addresses" % ifname)
+                     return -1
+         retry -= 1
+         return retry
+ 
+     def _up(self, ifaceobj):
++        real_ifname = self.cache.link_translate_altname(ifaceobj.name)
++
+         # if dhclient is already running do not stop and start it
+-        dhclient4_running = self.dhclientcmd.is_running(ifaceobj.name)
+-        dhclient6_running = self.dhclientcmd.is_running6(ifaceobj.name)
++        dhcp4_running = self.dhcpcmd.is_running(real_ifname)
++        dhcp6_running = 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 need
+         # to release/kill the appropriate dhclient(4/6) if they are running
+-        self._down_stale_dhcp_config(ifaceobj, 'inet', dhclient4_running)
+-        self._down_stale_dhcp_config(ifaceobj, 'inet6', dhclient6_running)
++        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-down yes" % ifaceobj.name)
+             return
+ 
+         try:
+-            dhclient_cmd_prefix = None
++            dhcp_cmd_prefix = []
+             dhcp_wait = policymanager.policymanager_api.get_attr_default(
+                 module_name=self.__class__.__name__, attr='dhcp-wait')
+             wait = str(dhcp_wait).lower() != "no"
+@@ -163,38 +188,43 @@ class dhcp(Addon, moduleBase):
+             vrf = ifaceobj.get_attr_value_first('vrf')
+             if (vrf and self.vrf_exec_cmd_prefix and
+                 self.cache.link_exists(vrf)):
+-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, vrf)
++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
+             elif self.mgmt_vrf_context:
+-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, 'default')
++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + ['default']
+                 self.logger.info('detected mgmt vrf context starting dhclient 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=wait,
+-                        cmd_prefix=dhclient_cmd_prefix
++                        cmd_prefix=dhcp_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, stopping')
++                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=dhcp6_duid)
++                        self.dhcpcmd.stop6(real_ifname, duid=dhcp6_duid)
+                     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 = utils.exec_command('%s -6 addr show %s'
+-                                                         %(utils.ip_cmd, ifaceobj.name))
+-                        r = re.search('inet6 .* scope link', addr_output)
+-                        if r:
+-                            self.dhclientcmd.start6(ifaceobj.name,
+-                                                    wait=wait,
+-                                                    cmd_prefix=dhclient_cmd_prefix, duid=dhcp6_duid)
+-                            return
++                        if self.cache.link_is_up(real_ifname):
++                            break
+                         timeout -= 1
+                         if timeout:
+                             time.sleep(1)
++
++                    self.dhcpcmd.start6(real_ifname,
++                                            wait=wait,
++                                            cmd_prefix=dhcp_cmd_prefix, duid=dhcp6_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, stopping')
++                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 = addr_family
+ 
+     def _dhcp_down(self, ifaceobj):
+-        dhclient_cmd_prefix = None
++        dhcp_cmd_prefix = []
++        ifname = self.cache.link_translate_altname(ifaceobj.name)
++
+         vrf = ifaceobj.get_attr_value_first('vrf')
+         if (vrf and self.vrf_exec_cmd_prefix and
+             self.cache.link_exists(vrf)):
+-            dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, vrf)
++            dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
+         dhcp6_duid = policymanager.policymanager_api.get_iface_default(module_name=self.__class__.__name__, \
+                                                                        ifname=ifaceobj.name, attr='dhcp6-duid')
+         if 'inet6' in ifaceobj.addr_family:
+-            self.dhclientcmd.release6(ifaceobj.name, dhclient_cmd_prefix, duid=dhcp6_duid)
++            self.dhcpcmd.release6(ifname, dhcp_cmd_prefix, duid=dhcp6_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 = ifaceStatus.SUCCESS
+         dhcp_running = False
++        ifname = self.cache.link_translate_altname(ifaceobjcurr.name)
+ 
+-        dhcp_v4 = self.dhclientcmd.is_running(ifaceobjcurr.name)
+-        dhcp_v6 = self.dhclientcmd.is_running6(ifaceobjcurr.name)
++        dhcp_v4 = self.dhcpcmd.is_running(ifname)
++        dhcp_v6 = self.dhcpcmd.is_running6(ifname)
+ 
+         if dhcp_v4:
+             dhcp_running = True
+@@ -271,12 +308,14 @@ class dhcp(Addon, moduleBase):
+         ifaceobjcurr.status = status
+ 
+     def _query_running(self, ifaceobjrunning):
+-        if not self.cache.link_exists(ifaceobjrunning.name):
++        ifname = self.cache.link_translate_altname(ifaceobjrunning.name)
++
++        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 = 'dhcp'
+-        if self.dhclientcmd.is_running6(ifaceobjrunning.name):
++        if self.dhcpcmd.is_running6(ifname):
+             ifaceobjrunning.addr_family.append('inet6')
+             ifaceobjrunning.addr_method = '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, ifaceLinkPrivFlags, 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, ifaceLinkPrivFlags, 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 = 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 = None
+         self.name = self.__class__.__name__
+         self.vrf_mgmt_devname = policymanager.policymanager_api.get_module_globals(
+             module_name=self.__class__.__name__,
+@@ -128,6 +130,11 @@ class vrf(Addon, moduleBase):
+             self.ip6_rule_cache = []
+             self.logger.warning('vrf: cache v6: %s' % str(e))
+ 
++        try:
++            self.dhcpcmd = DhcpClient()
++        except NoDhcpClientAvailable as e:
++            self.dhcpcmd = 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=None):
+         """ If we have a vrf slave that has dhcp configured, bring up the
+             vrf master now. This is needed because vrf has special handling
+-            in dhclient hook which requires the vrf master to be present """
++            in dhcp hook which requires the vrf master to be present """
+         vrf_master = 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 = None
++            dhcp_cmd_prefix = None
+             if (vrfname and self.vrf_exec_cmd_prefix and
+                 self.cache.link_exists(vrfname)):
+-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix,
+-                                                vrfname)
+-            self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix)
++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrfname]
++
++            ifacename = self.cache.link_translate_altname(ifaceobj.name)
++            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 = dhclient()
+-
+     def run(self, ifaceobj, operation, query_ifaceobj=None,
+             ifaceobj_getfunc=None, **extra_args):
+         """ run bond configuration on the interface object passed as argument
+@@ -1152,7 +1156,7 @@ class vrf(Addon, moduleBase):
+         op_handler = self._run_ops.get(operation)
+         if not op_handler:
+             return
+-        self._init_command_handlers()
++
+         if operation == 'query-checkcurr':
+             op_handler(self, ifaceobj, query_ifaceobj)
+         else:
+diff --git a/ifupdown2/ifupdown/exceptions.py b/ifupdown2/ifupdown/exceptions.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     = '/sbin/ethtool'
+     systemctl_cmd   = '/bin/systemctl'
+     dpkg_cmd        = '/usr/bin/dpkg'
++    dhcpcd_cmd      = '/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/ifupdownaddons/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 dhclient
+     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 == errno.EACCES:
+-                        return os.path.exists("/proc/%s" % self.read_file_oneline(pidfilename))
+-                except Exception:
+-                    return False
+-            except Exception:
+-                return False
+-        return False
++    MAX_RETRIES = 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/dhclient3 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 = 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=None):
+-        if not cmd_prefix:
+-            cmd_aslist = []
+-        else:
+-            cmd_aslist = cmd_prefix.split()
+-        if cmd_aslist:
+-            cmd_aslist.extend(cmd)
+-        else:
+-            cmd_aslist = cmd
+-        utils.exec_commandl(cmd_aslist, stdout=None, stderr=None)
++    def is_running6(self, ifacename: str) -> bool:
++        pid = 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] = []):
++        self.logger.debug(f'stopping dhclient on {ifacename}')
+ 
+-    def stop(self, ifacename, cmd_prefix=None):
+         if os.path.exists('/sbin/dhclient3'):
+             cmd = ['/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=None, stderr=None)
+ 
+-    def start(self, ifacename, wait=True, cmd_prefix=None):
++    def start(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = []):
++        self.logger.debug(f'starting dhclient on {ifacename}')
+         retries = 0
+-        out = "0"
+ 
+         # wait if interface isn't up yet
+-        while '1' not in out and retries < 5:
+-            path = '/sys/class/net/%s/carrier' %ifacename
+-            out = 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 += 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=None, stderr=None)
++
++    def release(self, ifacename: str, cmd_prefix: list[str] = []):
++        self.logger.debug(f'releasing lease on {ifacename}')
+ 
+-    def release(self, ifacename, cmd_prefix=None):
+         if os.path.exists('/sbin/dhclient3'):
+             cmd = ['/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=None, stderr=None)
++
++    def start6(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = [], duid: str | None = None):
++        self.logger.debug(f'starting v6 dhclient on {ifacename}')
+ 
+-    def start6(self, ifacename, wait=True, cmd_prefix=None, duid=None):
+         cmd = ['/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=None, stderr=None)
++
++    def stop6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
++        self.logger.debug(f'stopping v6 dhclient on {ifacename}')
+ 
+-    def stop6(self, ifacename, cmd_prefix=None, duid=None):
+         cmd = ['/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=None, stderr=None)
++
++    def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
++        self.logger.debug(f'releasing v6 lease on {ifacename}')
+ 
+-    def release6(self, ifacename, cmd_prefix=None, duid=None):
+         cmd = ['/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=None, stderr=None)
+diff --git a/ifupdown2/ifupdownaddons/dhcp_client.py b/ifupdown2/ifupdownaddons/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 over dhclient)
++    and forwards all method calls to the respective DHCP client.
++    """
++
++    impl: DhcpcdClient | dhclient
++
++    _PROXIED_METHODS = [
++        'is_running',
++        'is_running6',
++        'start',
++        'start6',
++        'stop',
++        'stop6',
++        'release',
++        'release6',
++    ]
++
++    def __init__(self, *args, **kwargs):
++        self.logger = logging.getLogger('ifupdown.dhcp_client')
++
++        try:
++            self.impl = DhcpcdClient(**kwargs)
++            self.logger.info('using dhcpcd client')
++        except RuntimeError:
++            self.logger.debug('dhcpcd client unavailable, trying deprecated dhclient')
++            try:
++                self.impl = dhclient(**kwargs)
++                self.logger.info('using deprecated dhclient client')
++            except RuntimeError:
++                raise NoDhcpClientAvailable('neither dhcpcd nor dhclient 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/ifupdownaddons/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) client
++    """
++    MAX_RETRIES = 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.dhcpcd_cmd}')
++
++    def _start(self, cmd_prefix: list[str], ifacename: str, wait: bool, ipv6: bool, duid: str | None = None):
++        """
++        Starts the dhcpcd(8) with the given arguments.
++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
++        :param ifacename: Interface name
++        :param wait: Whether to wait until a lease has been acquired before 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 = 0
++
++        # wait if interface isn't up yet
++        while not Sysfs.link_has_carrier(ifacename) and retries < self.MAX_RETRIES:
++            retries += 1
++            time.sleep(1)
++
++        cmd = [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=None)
++
++    def is_running(self, ifacename: str) -> bool:
++        """
++        Checks whether an IPv4 dhcpcd(8) daemon is running for the given interface.
++        :param ifacename: Interface name
++        """
++        pid = 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 given interface.
++        :param ifacename: Interface name
++        """
++        pid = 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 = True, cmd_prefix: list[str] = []):
++        """
++        Starts the dhcpcd(8) for leasing an IPv4 address.
++        :param ifacename: Interface name
++        :param wait: Whether to wait until a lease has been acquired before dhcpcd becomes a daemon.
++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
++        """
++        self.logger.debug(f'starting dhcpcd client on {ifacename}')
++        self._start(cmd_prefix, ifacename, wait, ipv6=False)
++
++    def stop(self, ifacename: str, cmd_prefix: list[str] = []):
++        """
++        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 dhcpcd command with
++        """
++        self.logger.debug(f'stopping dhcpcd client on {ifacename}')
++
++        cmd = [utils.dhcpcd_cmd, '--ipv4only', '--exit', ifacename]
++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
++
++    def release(self, ifacename: str, cmd_prefix: list[str] = []):
++        """
++        Releases the current IPv4 lease for the given interface.
++        :param ifacename: Interface name
++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
++        """
++        self.logger.debug(f'releasing lease on {ifacename}')
++
++        cmd = [utils.dhcpcd_cmd, '--ipv4only', '--release', ifacename]
++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
++
++    def start6(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = [], duid: str | None = 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 before dhcpcd becomes a daemon.
++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd 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] = [], duid: str | None = 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 dhcpcd 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 = [utils.dhcpcd_cmd, '--ipv6only', '--exit', ifacename]
++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
++
++    def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
++        """
++        Releases the current IPv4 lease for the given interface.
++        :param ifacename: Interface name
++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
++        :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
++        """
++        self.logger.debug(f'releasing v6 lease on {ifacename}')
++
++        cmd = [utils.dhcpcd_cmd, '--ipv6only', '--release', ifacename]
++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
+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" == self.read_file_oneline("/sys/class/net/%s/operstate" % ifname)
+ 
++    def link_has_carrier(self, name: str) -> bool:
++        """
++        Checks whether the given interface has CARRIER set
++        """
++        out = 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
+
-- 
2.51.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

* [pve-devel] [PATCH ifupdown2 2/2] gitignore: add build artifacts
  2025-12-12 11:30 [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss
@ 2025-12-12 11:30 ` Christoph Heiss
  2026-02-10 10:51 ` [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss
  1 sibling, 0 replies; 3+ messages in thread
From: Christoph Heiss @ 2025-12-12 11:30 UTC (permalink / raw)
  To: pve-devel

Much the same as we have in most other repos.

Signed-off-by: Christoph Heiss <c.heiss@proxmox.com>
---
 .gitignore | 4 ++++
 1 file changed, 4 insertions(+)
 create mode 100644 .gitignore

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..81ebd34
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,4 @@
+*.deb
+*.buildinfo
+*.changes
+/ifupdown2-[0-9]*/
-- 
2.51.2



_______________________________________________
pve-devel mailing list
pve-devel@lists.proxmox.com
https://lists.proxmox.com/cgi-bin/mailman/listinfo/pve-devel


^ permalink raw reply	[flat|nested] 3+ messages in thread

* Re: [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client
  2025-12-12 11:30 [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss
  2025-12-12 11:30 ` [pve-devel] [PATCH ifupdown2 2/2] gitignore: add build artifacts Christoph Heiss
@ 2026-02-10 10:51 ` Christoph Heiss
  1 sibling, 0 replies; 3+ messages in thread
From: Christoph Heiss @ 2026-02-10 10:51 UTC (permalink / raw)
  To: Proxmox VE development discussion

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 <c.heiss@proxmox.com>
> ---
> 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-backslash-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-slave.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-dhcpcd-8-as-dhcp-client.patch b/debian/patches/upstream/0003-addons-dhcp-add-support-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-as-dhcp-client.patch
> @@ -0,0 +1,1009 @@
> +From c96ed60826714873ad06315bdfa9ec6028fd2f08 Mon Sep 17 00:00:00 2001
> +From: Christoph Heiss <c.heiss@proxmox.com>
> +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 <c.heiss@proxmox.com>
> +---
> + 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/addonshelperapiref.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
> ++======
> ++
> ++Helper module to interact with the dhcpcd(8) DHCP client.
> ++
> ++.. automodule:: dhcpcd
> ++
> ++.. autoclass:: dhcpcd
> ++
> ++DhcpClient
> ++==========
> ++
> ++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, ifaceLinkPrivFlags, 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, ifaceLinkPrivFlags, 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 = "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 = 1
> +
> ++        try:
> ++            self.dhcpcmd = DhcpClient()
> ++        except NoDhcpClientAvailable as e:
> ++            self.dhcpcmd = None
> ++
> +     def __policy_get_default_mtu(self):
> +         default_mtu = policymanager.policymanager_api.get_attr_default(
> +             module_name=self.__class__.__name__,
> +@@ -1207,18 +1216,17 @@ class address(AddonWithIpBlackList, moduleBase):
> +             if (addr_method not in ["dhcp", "ppp"]  and not ifupdownflags.flags.PERFMODE and
> +                     not (ifaceobj.flags & iface.HAS_SIBLINGS)):
> +                 # if not running in perf mode and ifaceobj does not have
> +-                # any sibling iface objects, kill any stale dhclient
> +-                # processes
> +-                dhclientcmd = 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 = True
> +-                elif dhclientcmd.is_running6(ifaceobj.name):
> +-                    dhclientcmd.release6(ifaceobj.name)
> +-                    self.cache.force_address_flush_family(ifaceobj.name, socket.AF_INET6)
> +-                    force_reapply = 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 = 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 = True
> +         except Exception:
> +             pass
> +
> +@@ -1624,9 +1632,8 @@ class address(AddonWithIpBlackList, moduleBase):
> +
> +         self.query_running_ipv6_addrgen(ifaceobjrunning)
> +
> +-        dhclientcmd = dhclient()
> +-        if (dhclientcmd.is_running(ifaceobjrunning.name) or
> +-                dhclientcmd.is_running6(ifaceobjrunning.name)):
> ++        if self.dhcpcmd and (self.dhcpcmd.is_running(ifaceobjrunning.name) 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, ifaceStatus
> +     from ifupdown2.ifupdown.utils import utils
> ++    from ifupdown2.ifupdown.exceptions import moduleNotSupported, NoDhcpClientAvailable
> +
> +-    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, NoDhcpClientAvailable
> +
> +-    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 = 0
> ++    # policy: dhcp_retry_on_failure
> ++    DHCP_RETRY_ON_FAILURE = 0
> ++
> ++    dhcpcmd: DhcpClient
> +
> +     def __init__(self, *args, **kargs):
> +         Addon.__init__(self)
> +         moduleBase.__init__(self, *args, **kargs)
> +-        self.dhclientcmd = dhclient(**kargs)
> +         vrf_id = self._get_vrf_context()
> +         if vrf_id and vrf_id == 'mgmt':
> +             self.mgmt_vrf_context = True
> +@@ -55,24 +58,41 @@ class dhcp(Addon, moduleBase):
> +         self.logger.info('mgmt vrf_context = %s' %self.mgmt_vrf_context)
> +
> +         try:
> +-            self.dhclient_retry_on_failure = int(
> +-                policymanager.policymanager_api.get_module_globals(
> +-                    module_name=self.__class__.__name__,
> +-                    attr="dhclient_retry_on_failure"
> ++            try:
> ++                self.dhcp_retry_on_failure = int(
> ++                    policymanager.policymanager_api.get_module_globals(
> ++                        module_name=self.__class__.__name__,
> ++                        attr="dhcp_retry_on_failure"
> ++                    )
> ++                )
> ++            except:
> ++                self.dhcp_retry_on_failure = int(
> ++                    policymanager.policymanager_api.get_module_globals(
> ++                        module_name=self.__class__.__name__,
> ++                        attr="dhclient_retry_on_failure"
> ++                    )
> +                 )
> +-            )
> +         except Exception:
> +-            self.dhclient_retry_on_failure = self.DHCLIENT_RETRY_ON_FAILURE
> ++            self.dhcp_retry_on_failure = self.DHCP_RETRY_ON_FAILURE
> +
> +-        if self.dhclient_retry_on_failure < 0:
> +-            self.dhclient_retry_on_failure = 0
> ++        if self.dhcp_retry_on_failure < 0:
> ++            self.dhcp_retry_on_failure = 0
> +
> +-        self.logger.debug("dhclient: dhclient_retry_on_failure set to %s" % self.dhclient_retry_on_failure)
> ++        try:
> ++            self.dhcpcmd = 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" % self.dhcp_retry_on_failure)
> +
> +     def syntax_check(self, ifaceobj, ifaceobj_getfunc):
> +         return self.is_dhcp_allowed_on(ifaceobj, syntax_check=True)
> +
> +     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=True)
> +         return True
> +@@ -97,9 +117,9 @@ class dhcp(Addon, moduleBase):
> +             pass
> +         return ips
> +
> +-    def dhclient_start_and_check(self, ifname, family, handler, wait=True, **handler_kwargs):
> ++    def dhcp_client_start_and_check(self, ifname, family, handler, wait=True, **handler_kwargs):
> +         ip_config_before = self.get_current_ip_configured(ifname, family)
> +-        retry = self.dhclient_retry_on_failure
> ++        retry = self.dhcp_retry_on_failure
> +
> +         while retry >= 0:
> +             handler(ifname, wait=wait, **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=False param.
> +                 return
> +-            retry = self.dhclient_check(ifname, family, ip_config_before, retry, handler_kwargs.get("cmd_prefix"))
> ++            retry = 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, dhclient_cmd_prefix):
> ++    def dhcp_client_check(self, ifname, family, ip_config_before, retry, dhcp_cmd_prefix: list[str]):
> +         diff = self.get_current_ip_configured(ifname, family).difference(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(diff))
> +             )
> +             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, retrying %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 detect new ip addresses" % ifname)
> +                     return -1
> +         retry -= 1
> +         return retry
> +
> +     def _up(self, ifaceobj):
> ++        real_ifname = self.cache.link_translate_altname(ifaceobj.name)
> ++
> +         # if dhclient is already running do not stop and start it
> +-        dhclient4_running = self.dhclientcmd.is_running(ifaceobj.name)
> +-        dhclient6_running = self.dhclientcmd.is_running6(ifaceobj.name)
> ++        dhcp4_running = self.dhcpcmd.is_running(real_ifname)
> ++        dhcp6_running = 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 need
> +         # to release/kill the appropriate dhclient(4/6) if they are running
> +-        self._down_stale_dhcp_config(ifaceobj, 'inet', dhclient4_running)
> +-        self._down_stale_dhcp_config(ifaceobj, 'inet6', dhclient6_running)
> ++        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-down yes" % ifaceobj.name)
> +             return
> +
> +         try:
> +-            dhclient_cmd_prefix = None
> ++            dhcp_cmd_prefix = []
> +             dhcp_wait = policymanager.policymanager_api.get_attr_default(
> +                 module_name=self.__class__.__name__, attr='dhcp-wait')
> +             wait = str(dhcp_wait).lower() != "no"
> +@@ -163,38 +188,43 @@ class dhcp(Addon, moduleBase):
> +             vrf = ifaceobj.get_attr_value_first('vrf')
> +             if (vrf and self.vrf_exec_cmd_prefix and
> +                 self.cache.link_exists(vrf)):
> +-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, vrf)
> ++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
> +             elif self.mgmt_vrf_context:
> +-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, 'default')
> ++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + ['default']
> +                 self.logger.info('detected mgmt vrf context starting dhclient 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=wait,
> +-                        cmd_prefix=dhclient_cmd_prefix
> ++                        cmd_prefix=dhcp_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, stopping')
> ++                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=dhcp6_duid)
> ++                        self.dhcpcmd.stop6(real_ifname, duid=dhcp6_duid)
> +                     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 = utils.exec_command('%s -6 addr show %s'
> +-                                                         %(utils.ip_cmd, ifaceobj.name))
> +-                        r = re.search('inet6 .* scope link', addr_output)
> +-                        if r:
> +-                            self.dhclientcmd.start6(ifaceobj.name,
> +-                                                    wait=wait,
> +-                                                    cmd_prefix=dhclient_cmd_prefix, duid=dhcp6_duid)
> +-                            return
> ++                        if self.cache.link_is_up(real_ifname):
> ++                            break
> +                         timeout -= 1
> +                         if timeout:
> +                             time.sleep(1)
> ++
> ++                    self.dhcpcmd.start6(real_ifname,
> ++                                            wait=wait,
> ++                                            cmd_prefix=dhcp_cmd_prefix, duid=dhcp6_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, stopping')
> ++                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 = addr_family
> +
> +     def _dhcp_down(self, ifaceobj):
> +-        dhclient_cmd_prefix = None
> ++        dhcp_cmd_prefix = []
> ++        ifname = self.cache.link_translate_altname(ifaceobj.name)
> ++
> +         vrf = ifaceobj.get_attr_value_first('vrf')
> +         if (vrf and self.vrf_exec_cmd_prefix and
> +             self.cache.link_exists(vrf)):
> +-            dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix, vrf)
> ++            dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrf]
> +         dhcp6_duid = policymanager.policymanager_api.get_iface_default(module_name=self.__class__.__name__, \
> +                                                                        ifname=ifaceobj.name, attr='dhcp6-duid')
> +         if 'inet6' in ifaceobj.addr_family:
> +-            self.dhclientcmd.release6(ifaceobj.name, dhclient_cmd_prefix, duid=dhcp6_duid)
> ++            self.dhcpcmd.release6(ifname, dhcp_cmd_prefix, duid=dhcp6_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 = ifaceStatus.SUCCESS
> +         dhcp_running = False
> ++        ifname = self.cache.link_translate_altname(ifaceobjcurr.name)
> +
> +-        dhcp_v4 = self.dhclientcmd.is_running(ifaceobjcurr.name)
> +-        dhcp_v6 = self.dhclientcmd.is_running6(ifaceobjcurr.name)
> ++        dhcp_v4 = self.dhcpcmd.is_running(ifname)
> ++        dhcp_v6 = self.dhcpcmd.is_running6(ifname)
> +
> +         if dhcp_v4:
> +             dhcp_running = True
> +@@ -271,12 +308,14 @@ class dhcp(Addon, moduleBase):
> +         ifaceobjcurr.status = status
> +
> +     def _query_running(self, ifaceobjrunning):
> +-        if not self.cache.link_exists(ifaceobjrunning.name):
> ++        ifname = self.cache.link_translate_altname(ifaceobjrunning.name)
> ++
> ++        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 = 'dhcp'
> +-        if self.dhclientcmd.is_running6(ifaceobjrunning.name):
> ++        if self.dhcpcmd.is_running6(ifname):
> +             ifaceobjrunning.addr_family.append('inet6')
> +             ifaceobjrunning.addr_method = '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, ifaceLinkPrivFlags, 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, ifaceLinkPrivFlags, 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 = 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 = None
> +         self.name = self.__class__.__name__
> +         self.vrf_mgmt_devname = policymanager.policymanager_api.get_module_globals(
> +             module_name=self.__class__.__name__,
> +@@ -128,6 +130,11 @@ class vrf(Addon, moduleBase):
> +             self.ip6_rule_cache = []
> +             self.logger.warning('vrf: cache v6: %s' % str(e))
> +
> ++        try:
> ++            self.dhcpcmd = DhcpClient()
> ++        except NoDhcpClientAvailable as e:
> ++            self.dhcpcmd = 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=None):
> +         """ If we have a vrf slave that has dhcp configured, bring up the
> +             vrf master now. This is needed because vrf has special handling
> +-            in dhclient hook which requires the vrf master to be present """
> ++            in dhcp hook which requires the vrf master to be present """
> +         vrf_master = 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 = None
> ++            dhcp_cmd_prefix = None
> +             if (vrfname and self.vrf_exec_cmd_prefix and
> +                 self.cache.link_exists(vrfname)):
> +-                dhclient_cmd_prefix = '%s %s' %(self.vrf_exec_cmd_prefix,
> +-                                                vrfname)
> +-            self.dhclientcmd.release(ifaceobj.name, dhclient_cmd_prefix)
> ++                dhcp_cmd_prefix = self.vrf_exec_cmd_prefix.split() + [vrfname]
> ++
> ++            ifacename = self.cache.link_translate_altname(ifaceobj.name)
> ++            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 = dhclient()
> +-
> +     def run(self, ifaceobj, operation, query_ifaceobj=None,
> +             ifaceobj_getfunc=None, **extra_args):
> +         """ run bond configuration on the interface object passed as argument
> +@@ -1152,7 +1156,7 @@ class vrf(Addon, moduleBase):
> +         op_handler = self._run_ops.get(operation)
> +         if not op_handler:
> +             return
> +-        self._init_command_handlers()
> ++
> +         if operation == 'query-checkcurr':
> +             op_handler(self, ifaceobj, query_ifaceobj)
> +         else:
> +diff --git a/ifupdown2/ifupdown/exceptions.py b/ifupdown2/ifupdown/exceptions.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     = '/sbin/ethtool'
> +     systemctl_cmd   = '/bin/systemctl'
> +     dpkg_cmd        = '/usr/bin/dpkg'
> ++    dhcpcd_cmd      = '/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/ifupdownaddons/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 dhclient
> +     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 == errno.EACCES:
> +-                        return os.path.exists("/proc/%s" % self.read_file_oneline(pidfilename))
> +-                except Exception:
> +-                    return False
> +-            except Exception:
> +-                return False
> +-        return False
> ++    MAX_RETRIES = 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/dhclient3 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 = 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=None):
> +-        if not cmd_prefix:
> +-            cmd_aslist = []
> +-        else:
> +-            cmd_aslist = cmd_prefix.split()
> +-        if cmd_aslist:
> +-            cmd_aslist.extend(cmd)
> +-        else:
> +-            cmd_aslist = cmd
> +-        utils.exec_commandl(cmd_aslist, stdout=None, stderr=None)
> ++    def is_running6(self, ifacename: str) -> bool:
> ++        pid = 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] = []):
> ++        self.logger.debug(f'stopping dhclient on {ifacename}')
> +
> +-    def stop(self, ifacename, cmd_prefix=None):
> +         if os.path.exists('/sbin/dhclient3'):
> +             cmd = ['/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=None, stderr=None)
> +
> +-    def start(self, ifacename, wait=True, cmd_prefix=None):
> ++    def start(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = []):
> ++        self.logger.debug(f'starting dhclient on {ifacename}')
> +         retries = 0
> +-        out = "0"
> +
> +         # wait if interface isn't up yet
> +-        while '1' not in out and retries < 5:
> +-            path = '/sys/class/net/%s/carrier' %ifacename
> +-            out = 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 += 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=None, stderr=None)
> ++
> ++    def release(self, ifacename: str, cmd_prefix: list[str] = []):
> ++        self.logger.debug(f'releasing lease on {ifacename}')
> +
> +-    def release(self, ifacename, cmd_prefix=None):
> +         if os.path.exists('/sbin/dhclient3'):
> +             cmd = ['/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=None, stderr=None)
> ++
> ++    def start6(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = [], duid: str | None = None):
> ++        self.logger.debug(f'starting v6 dhclient on {ifacename}')
> +
> +-    def start6(self, ifacename, wait=True, cmd_prefix=None, duid=None):
> +         cmd = ['/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=None, stderr=None)
> ++
> ++    def stop6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
> ++        self.logger.debug(f'stopping v6 dhclient on {ifacename}')
> +
> +-    def stop6(self, ifacename, cmd_prefix=None, duid=None):
> +         cmd = ['/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=None, stderr=None)
> ++
> ++    def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
> ++        self.logger.debug(f'releasing v6 lease on {ifacename}')
> +
> +-    def release6(self, ifacename, cmd_prefix=None, duid=None):
> +         cmd = ['/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=None, stderr=None)
> +diff --git a/ifupdown2/ifupdownaddons/dhcp_client.py b/ifupdown2/ifupdownaddons/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 over dhclient)
> ++    and forwards all method calls to the respective DHCP client.
> ++    """
> ++
> ++    impl: DhcpcdClient | dhclient
> ++
> ++    _PROXIED_METHODS = [
> ++        'is_running',
> ++        'is_running6',
> ++        'start',
> ++        'start6',
> ++        'stop',
> ++        'stop6',
> ++        'release',
> ++        'release6',
> ++    ]
> ++
> ++    def __init__(self, *args, **kwargs):
> ++        self.logger = logging.getLogger('ifupdown.dhcp_client')
> ++
> ++        try:
> ++            self.impl = DhcpcdClient(**kwargs)
> ++            self.logger.info('using dhcpcd client')
> ++        except RuntimeError:
> ++            self.logger.debug('dhcpcd client unavailable, trying deprecated dhclient')
> ++            try:
> ++                self.impl = dhclient(**kwargs)
> ++                self.logger.info('using deprecated dhclient client')
> ++            except RuntimeError:
> ++                raise NoDhcpClientAvailable('neither dhcpcd nor dhclient 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/ifupdownaddons/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) client
> ++    """
> ++    MAX_RETRIES = 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.dhcpcd_cmd}')
> ++
> ++    def _start(self, cmd_prefix: list[str], ifacename: str, wait: bool, ipv6: bool, duid: str | None = None):
> ++        """
> ++        Starts the dhcpcd(8) with the given arguments.
> ++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
> ++        :param ifacename: Interface name
> ++        :param wait: Whether to wait until a lease has been acquired before 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 = 0
> ++
> ++        # wait if interface isn't up yet
> ++        while not Sysfs.link_has_carrier(ifacename) and retries < self.MAX_RETRIES:
> ++            retries += 1
> ++            time.sleep(1)
> ++
> ++        cmd = [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=None)
> ++
> ++    def is_running(self, ifacename: str) -> bool:
> ++        """
> ++        Checks whether an IPv4 dhcpcd(8) daemon is running for the given interface.
> ++        :param ifacename: Interface name
> ++        """
> ++        pid = 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 given interface.
> ++        :param ifacename: Interface name
> ++        """
> ++        pid = 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 = True, cmd_prefix: list[str] = []):
> ++        """
> ++        Starts the dhcpcd(8) for leasing an IPv4 address.
> ++        :param ifacename: Interface name
> ++        :param wait: Whether to wait until a lease has been acquired before dhcpcd becomes a daemon.
> ++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
> ++        """
> ++        self.logger.debug(f'starting dhcpcd client on {ifacename}')
> ++        self._start(cmd_prefix, ifacename, wait, ipv6=False)
> ++
> ++    def stop(self, ifacename: str, cmd_prefix: list[str] = []):
> ++        """
> ++        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 dhcpcd command with
> ++        """
> ++        self.logger.debug(f'stopping dhcpcd client on {ifacename}')
> ++
> ++        cmd = [utils.dhcpcd_cmd, '--ipv4only', '--exit', ifacename]
> ++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++    def release(self, ifacename: str, cmd_prefix: list[str] = []):
> ++        """
> ++        Releases the current IPv4 lease for the given interface.
> ++        :param ifacename: Interface name
> ++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
> ++        """
> ++        self.logger.debug(f'releasing lease on {ifacename}')
> ++
> ++        cmd = [utils.dhcpcd_cmd, '--ipv4only', '--release', ifacename]
> ++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++    def start6(self, ifacename: str, wait: bool = True, cmd_prefix: list[str] = [], duid: str | None = 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 before dhcpcd becomes a daemon.
> ++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd 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] = [], duid: str | None = 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 dhcpcd 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 = [utils.dhcpcd_cmd, '--ipv6only', '--exit', ifacename]
> ++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> ++
> ++    def release6(self, ifacename: str, cmd_prefix: list[str] = [], duid: str | None = None):
> ++        """
> ++        Releases the current IPv4 lease for the given interface.
> ++        :param ifacename: Interface name
> ++        :param cmd_prefix: Optional list of strings to prefix the dhcpcd command with
> ++        :param duid: Optional DUID type for IPv6, must be 'LL' or 'LLT'
> ++        """
> ++        self.logger.debug(f'releasing v6 lease on {ifacename}')
> ++
> ++        cmd = [utils.dhcpcd_cmd, '--ipv6only', '--release', ifacename]
> ++        utils.exec_commandl(cmd_prefix + cmd, stdout=None)
> +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" == self.read_file_oneline("/sys/class/net/%s/operstate" % ifname)
> +
> ++    def link_has_carrier(self, name: str) -> bool:
> ++        """
> ++        Checks whether the given interface has CARRIER set
> ++        """
> ++        out = 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
> +





^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2026-02-10 10:51 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2025-12-12 11:30 [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss
2025-12-12 11:30 ` [pve-devel] [PATCH ifupdown2 2/2] gitignore: add build artifacts Christoph Heiss
2026-02-10 10:51 ` [pve-devel] [PATCH ifupdown2 1/2] d/patches: add patch for supporting dhcpcd as alternative dhcp client Christoph Heiss

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox
Service provided by Proxmox Server Solutions GmbH | Privacy | Legal