Protocols will notify when dhcpcd can exit (#536)

* Protocols will notify when dhcpcd can exit

DHCPv6 RELEASE requires the addresses to be dropped before
a RELEASE message is sent.
We now wait for an acknowledgement or a timeout before notifying
that DHCPv6 has stopped for the interface.

DHCPv4 RELEASE is the other way around, there is no acknowledgement.
So we wait for 1 second after sending the message before removing
the address and notifying DHCP has stopped for the interface.

If we are not releasing then we notify dhcpcd that the protocol has
stopped right away when we drop the lease.

dhcpcd will exit once there are no running protocols for the
interfaces.

Fixes #513.
Hopefully #535, #519 and #509 as well.

Co-authored-by: Sime Zupanovic (EXT) <sime.zupanovic.ext@ericsson.com>
This commit is contained in:
Roy Marples 2025-11-11 13:13:03 +00:00 committed by GitHub
parent b573b9d87b
commit 665b573d47
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 267 additions and 93 deletions

View File

@ -22,7 +22,7 @@
.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
.\" SUCH DAMAGE.
.\"
.Dd October 11, 2024
.Dd October 6, 2025
.Dt DHCPCD-RUN-HOOKS 8
.Os
.Sh NAME
@ -105,6 +105,8 @@ dhcpcd renewed its lease.
dhcpcd has rebound to a new DHCP server.
.It Dv REBOOT | Dv REBOOT6
dhcpcd successfully requested a lease from a DHCP server.
.It Dv RELEASE | Dv RELEASE6
dhcpcd has released the lease.
.It Dv DELEGATED6
dhcpcd assigned a delegated prefix to the interface.
.It Dv IPV4LL

View File

@ -2866,6 +2866,51 @@ dhcp_reboot(struct interface *ifp)
send_request(ifp);
}
static void
dhcp_deconfigure(void *arg)
{
struct interface *ifp = arg;
struct dhcp_state *state = D_STATE(ifp);
struct if_options *ifo = ifp->options;
const char *reason;
#ifdef AUTH
dhcp_auth_reset(&state->auth);
#endif
if (state->state == DHS_RELEASE)
reason = "RELEASE";
else
reason = state->reason;
state->state = DHS_NONE;
free(state->offer);
state->offer = NULL;
state->offer_len = 0;
free(state->old);
state->old = state->new;
state->old_len = state->new_len;
state->new = NULL;
state->new_len = 0;
if (ifo->options & DHCPCD_CONFIGURE)
ipv4_applyaddr(ifp);
else {
state->addr = NULL;
state->added = 0;
}
script_runreason(ifp, reason);
free(state->old);
state->old = NULL;
state->old_len = 0;
state->lease.addr.s_addr = 0;
ifo->options &= ~(DHCPCD_CSR_WARNED | DHCPCD_ROUTER_HOST_ROUTE_WARNED);
if (ifo->options & DHCPCD_STOPPING) {
dhcp_free(ifp);
dhcpcd_dropped(ifp);
} else
dhcp_close(ifp);
}
void
dhcp_drop(struct interface *ifp, const char *reason)
{
@ -2876,6 +2921,7 @@ dhcp_drop(struct interface *ifp, const char *reason)
* but we do have a timeout, so punt it. */
if (state == NULL || state->state == DHS_NONE) {
eloop_timeout_delete(ifp->ctx->eloop, NULL, ifp);
dhcpcd_dropped(ifp);
return;
}
@ -2886,6 +2932,7 @@ dhcp_drop(struct interface *ifp, const char *reason)
#ifdef ARPING
state->arping_index = -1;
#endif
state->reason = reason;
if (ifo->options & DHCPCD_RELEASE && !(ifo->options & DHCPCD_INFORM)) {
/* Failure to send the release may cause this function to
@ -2899,10 +2946,21 @@ dhcp_drop(struct interface *ifp, const char *reason)
state->new != NULL &&
state->lease.server.s_addr != INADDR_ANY)
{
/* We need to delay removal of the IP address so the
* message can be sent.
* Unlike DHCPv6, there is no acknowledgement. */
const struct timespec delay = {
.tv_sec = 1,
};
loginfox("%s: releasing lease of %s",
ifp->name, inet_ntoa(state->lease.addr));
dhcp_new_xid(ifp);
send_message(ifp, DHCP_RELEASE, NULL);
eloop_timeout_delete(ifp->ctx->eloop, NULL, ifp);
eloop_timeout_add_tv(ifp->ctx->eloop,
&delay, dhcp_deconfigure, ifp);
return;
}
}
#ifdef AUTH
@ -2919,36 +2977,7 @@ dhcp_drop(struct interface *ifp, const char *reason)
#endif
eloop_timeout_delete(ifp->ctx->eloop, NULL, ifp);
#ifdef AUTH
dhcp_auth_reset(&state->auth);
#endif
state->state = DHS_NONE;
free(state->offer);
state->offer = NULL;
state->offer_len = 0;
free(state->old);
state->old = state->new;
state->old_len = state->new_len;
state->new = NULL;
state->new_len = 0;
state->reason = reason;
if (ifo->options & DHCPCD_CONFIGURE)
ipv4_applyaddr(ifp);
else {
state->addr = NULL;
state->added = 0;
script_runreason(ifp, state->reason);
}
free(state->old);
state->old = NULL;
state->old_len = 0;
state->lease.addr.s_addr = 0;
ifo->options &= ~(DHCPCD_CSR_WARNED | DHCPCD_ROUTER_HOST_ROUTE_WARNED);
/* Close DHCP ports so a changed interface family is picked
* up by a new BPF state. */
dhcp_close(ifp);
dhcp_deconfigure(ifp);
}
static int
@ -3108,6 +3137,12 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len,
#define IS_STATE_ACTIVE(s) ((s)-state != DHS_NONE && \
(s)->state != DHS_INIT && (s)->state != DHS_BOUND)
/* Don't do anything if the user hasn't configured it. */
if (ifp->active != IF_ACTIVE_USER ||
ifp->options->options & DHCPCD_STOPPING ||
!(ifp->options->options & DHCPCD_DHCP))
return;
if (bootp->op != BOOTREPLY) {
if (IS_STATE_ACTIVE(state))
logdebugx("%s: op (%d) is not BOOTREPLY",
@ -3932,6 +3967,7 @@ dhcp_free(struct interface *ifp)
free(state->offer);
free(state->clientid);
free(state);
ifp->if_data[IF_DATA_DHCP] = NULL;
}
ctx = ifp->ctx;

View File

@ -2110,29 +2110,25 @@ dhcp6_startrelease(struct interface *ifp)
struct dhcp6_state *state;
state = D6_STATE(ifp);
if (state->state != DH6S_BOUND)
if (state->state != DH6S_BOUND) {
dhcp6_finishrelease(ifp);
return;
}
state->state = DH6S_RELEASE;
state->RTC = 0;
state->IMD = REL_MAX_DELAY;
state->IRT = REL_TIMEOUT;
state->MRT = REL_MAX_RT;
/* MRC of REL_MAX_RC is optional in RFC 3315 18.1.6 */
#if 0
state->MRC = REL_MAX_RC;
state->MRCcallback = dhcp6_finishrelease;
#else
state->MRC = 0;
state->MRCcallback = NULL;
#endif
if (dhcp6_makemessage(ifp) == -1)
if (dhcp6_makemessage(ifp) == -1) {
logerr("%s: %s", __func__, ifp->name);
else {
dhcp6_sendrelease(ifp);
/* not much we can do apart from finish now */
dhcp6_finishrelease(ifp);
}
} else
dhcp6_sendrelease(ifp);
}
static int
@ -3610,6 +3606,11 @@ dhcp6_recvif(struct interface *ifp, const char *sfrom,
ifp->name, sfrom);
dhcp6_fail(ifp, true);
return;
case DH6S_RELEASE:
loginfox("%s: %s acknowledged RELEASE6",
ifp->name, sfrom);
dhcp6_finishrelease(ifp);
return;
default:
valid_op = false;
break;
@ -4293,6 +4294,7 @@ dhcp6_freedrop(struct interface *ifp, int drop, const char *reason)
free(state);
ifp->if_data[IF_DATA_DHCP6] = NULL;
}
dhcpcd_dropped(ifp);
/* If we don't have any more DHCP6 enabled interfaces,
* close the global socket and release resources */

View File

@ -438,27 +438,62 @@ dhcpcd_drop(struct interface *ifp, int stop)
dhcpcd_drop_af(ifp, stop, AF_UNSPEC);
}
static void
stop_interface(struct interface *ifp, const char *reason)
static bool
dhcpcd_ifrunning(struct interface *ifp)
{
struct dhcpcd_ctx *ctx;
ctx = ifp->ctx;
loginfox("%s: removing interface", ifp->name);
ifp->options->options |= DHCPCD_STOPPING;
#ifdef INET
if (D_CSTATE(ifp) != NULL)
return true;
#ifdef IPV4LL
if (IPV4LL_CSTATE(ifp) != NULL)
return true;
#endif
#endif
#ifdef DHCP6
if (D6_CSTATE(ifp) != NULL)
return true;
#endif
return false;
}
dhcpcd_drop(ifp, 1);
script_runreason(ifp, reason == NULL ? "STOPPED" : reason);
void
dhcpcd_dropped(struct interface *ifp)
{
struct dhcpcd_ctx *ctx = ifp->ctx;
/* Delete all timeouts for the interfaces */
eloop_q_timeout_delete(ctx->eloop, ELOOP_QUEUE_ALL, NULL, ifp);
if (ifp->options == NULL ||
!(ifp->options->options & DHCPCD_STOPPING) ||
dhcpcd_ifrunning(ifp))
return;
/* De-activate the interface */
ifp->active = IF_INACTIVE;
ifp->options->options &= ~DHCPCD_STOPPING;
if (ifp->active) {
ifp->active = IF_INACTIVE;
ifp->options->options &= ~DHCPCD_STOPPING;
script_runreason(ifp, "STOPPED");
}
if (!(ctx->options & (DHCPCD_MANAGER | DHCPCD_TEST)))
eloop_exit(ctx->eloop, EXIT_FAILURE);
if (!(ctx->options & DHCPCD_EXITING))
return;
TAILQ_FOREACH(ifp, ctx->ifaces, next) {
if (dhcpcd_ifrunning(ifp))
break;
}
/* All interfaces have stopped, we can exit */
if (ifp == NULL)
eloop_exit(ctx->eloop, EXIT_SUCCESS);
}
static void
stop_interface(struct interface *ifp)
{
loginfox("%s: removing interface", ifp->name);
ifp->options->options |= DHCPCD_STOPPING;
dhcpcd_drop(ifp, 1);
}
static void
@ -744,6 +779,17 @@ dhcpcd_handlecarrier(struct interface *ifp, int carrier, unsigned int flags)
ifp->carrier = carrier;
ifp->flags = flags;
/*
* Inactive interfaces may not have options, we so check the
* global context if we are stopping or not.
* This allows an active interface to stop even if dhcpcd is not.
*/
if (ifp->options != NULL) {
if (ifp->options->options & DHCPCD_STOPPING)
return;
} else if (ifp->ctx->options & DHCPCD_EXITING)
return;
if (!if_is_link_up(ifp)) {
if (!ifp->active || (!was_link_up && !was_roaming))
return;
@ -1070,13 +1116,16 @@ dhcpcd_handleinterface(void *arg, int action, const char *ifname)
}
if (ifp->active) {
logdebugx("%s: interface departed", ifp->name);
stop_interface(ifp, "DEPARTED");
stop_interface(ifp);
}
TAILQ_REMOVE(ctx->ifaces, ifp, next);
if_free(ifp);
return 0;
}
if (ctx->options & DHCPCD_EXITING)
return 0;
ifs = if_discover(ctx, &ifaddrs, -1, UNCONST(argv));
if (ifs == NULL) {
logerr(__func__);
@ -1378,14 +1427,15 @@ reconf_reboot(struct dhcpcd_ctx *ctx, int action, int argc, char **argv, int oi)
}
}
static void
static bool
stop_all_interfaces(struct dhcpcd_ctx *ctx, unsigned long long opts)
{
struct interface *ifp;
bool anystopped = false;
ctx->options |= opts;
if (ctx->ifaces == NULL)
return;
return anystopped;
if (ctx->options & DHCPCD_RELEASE)
ctx->options &= ~DHCPCD_PERSISTENT;
@ -1398,8 +1448,10 @@ stop_all_interfaces(struct dhcpcd_ctx *ctx, unsigned long long opts)
if (ifp->options->options & DHCPCD_RELEASE)
ifp->options->options &= ~DHCPCD_PERSISTENT;
ifp->options->options |= DHCPCD_EXITING;
stop_interface(ifp, NULL);
anystopped = true;
stop_interface(ifp);
}
return anystopped;
}
static void
@ -1437,7 +1489,6 @@ dhcpcd_renew(struct dhcpcd_ctx *ctx)
#ifdef USE_SIGNALS
#define sigmsg "received %s, %s"
static volatile bool dhcpcd_exiting = false;
void
dhcpcd_signal_cb(int sig, void *arg)
{
@ -1517,14 +1568,19 @@ dhcpcd_signal_cb(int sig, void *arg)
* During teardown we don't want to process SIGTERM or SIGINT again,
* as that could trigger memory issues.
*/
if (dhcpcd_exiting)
if (ctx->options & DHCPCD_EXITING)
return;
dhcpcd_exiting = true;
if (!(ctx->options & DHCPCD_TEST))
stop_all_interfaces(ctx, opts);
ctx->options |= DHCPCD_EXITING;
if (!(ctx->options & DHCPCD_TEST) &&
stop_all_interfaces(ctx, opts))
{
/* We stopped something, we will exit once that is done. */
eloop_exitallinners(exit_code);
return;
}
eloop_exitall(exit_code);
dhcpcd_exiting = false;
}
#endif
@ -1664,8 +1720,11 @@ dumperr:
if (opts & (DHCPCD_EXITING | DHCPCD_RELEASE)) {
if (oifind == argc && af == AF_UNSPEC) {
stop_all_interfaces(ctx, opts);
eloop_exit(ctx->eloop, EXIT_SUCCESS);
ctx->options |= DHCPCD_EXITING;
if (stop_all_interfaces(ctx, opts) == false)
eloop_exit(ctx->eloop, EXIT_SUCCESS);
/* We did stop an interface, it will notify us once
* dropped so we can exit. */
return 0;
}
@ -1695,7 +1754,7 @@ dumperr:
if (af != AF_UNSPEC)
dhcpcd_drop_af(ifp, 1, af);
else
stop_interface(ifp, NULL);
stop_interface(ifp);
ifo->options = orig_opts;
}
return 0;
@ -1897,17 +1956,28 @@ dhcpcd_pidfile_timeout(void *arg)
pid_t pid;
pid = pidfile_read(ctx->pidfile);
if(pid == -1)
if (pid == -1)
eloop_exit(ctx->eloop, EXIT_SUCCESS);
else if (++ctx->duid_len >= 100) { /* overload duid_len */
logerrx("pid %d failed to exit", (int)pid);
eloop_exit(ctx->eloop, EXIT_FAILURE);
} else
else
eloop_timeout_add_msec(ctx->eloop, 100,
dhcpcd_pidfile_timeout, ctx);
}
static void
dhcpcd_exit_timeout(void *arg)
{
struct dhcpcd_ctx *ctx = arg;
pid_t pid;
pid = pidfile_read(ctx->pidfile);
if (pid == -1)
eloop_exit(ctx->eloop, EXIT_SUCCESS);
else {
logwarnx("pid %lld failed to exit", (long long)pid);
eloop_exit(ctx->eloop, EXIT_FAILURE);
}
}
static int dup_null(int fd)
{
int fd_null = open(_PATH_DEVNULL, O_WRONLY);
@ -2271,6 +2341,8 @@ printpidfile:
/* Spin until it exits */
loginfox("waiting for pid %d to exit", (int)pid);
dhcpcd_pidfile_timeout(&ctx);
eloop_timeout_add_sec(ctx.eloop, 50,
dhcpcd_exit_timeout, &ctx);
goto run_loop;
}
}
@ -2723,7 +2795,7 @@ exit1:
if (ctx.options & DHCPCD_STARTED && !(ctx.options & DHCPCD_FORKED))
loginfox(PACKAGE " exited");
#ifdef PRIVSEP
if (ctx.ps_root != NULL && ps_root_stop(&ctx) == -1)
if (ps_root_stop(&ctx) == -1)
i = EXIT_FAILURE;
eloop_free(ctx.ps_eloop);
#endif

View File

@ -267,6 +267,7 @@ void dhcpcd_handlecarrier(struct interface *, int, unsigned int);
int dhcpcd_handleinterface(void *, int, const char *);
void dhcpcd_handlehwaddr(struct interface *, uint16_t, const void *, uint8_t);
void dhcpcd_dropinterface(struct interface *, const char *);
void dhcpcd_dropped(struct interface *);
int dhcpcd_selectprofile(struct interface *, const char *);
void dhcpcd_startinterface(void *);

View File

@ -46,8 +46,13 @@
#define KEVENT_N int
#endif
#elif defined(__linux__)
#include <linux/version.h>
#include <sys/epoll.h>
#include <poll.h>
#define USE_EPOLL
#if LINUX_VERSION_CODE >= KERNEL_VERSION(5, 11, 0)
#define HAVE_EPOLL_PWAIT2
#endif
#else
#include <poll.h>
#define USE_PPOLL
@ -143,6 +148,9 @@ struct eloop {
bool exitnow;
bool events_need_setup;
bool events_invalid;
#ifdef HAVE_EPOLL_PWAIT2
bool epoll_pwait2_nosys;
#endif
};
TAILQ_HEAD(eloop_head, eloop) eloops = TAILQ_HEAD_INITIALIZER(eloops);
@ -617,6 +625,19 @@ eloop_exitall(int code)
}
}
void
eloop_exitallinners(int code)
{
struct eloop *eloop;
TAILQ_FOREACH(eloop, &eloops, next) {
if (eloop == TAILQ_FIRST(&eloops))
continue;
eloop->exitcode = code;
eloop->exitnow = true;
}
}
#if defined(USE_KQUEUE) || defined(USE_EPOLL)
static int
eloop_open(struct eloop *eloop)
@ -924,24 +945,48 @@ eloop_run_kqueue(struct eloop *eloop, const struct timespec *ts)
static int
eloop_run_epoll(struct eloop *eloop, const struct timespec *ts)
{
int timeout, n, nn;
int n, nn;
struct epoll_event *epe;
struct eloop_event *e;
unsigned short events;
if (ts != NULL) {
if (ts->tv_sec > INT_MAX / 1000 ||
(ts->tv_sec == INT_MAX / 1000 &&
((ts->tv_nsec + 999999) / 1000000 > INT_MAX % 1000000)))
timeout = INT_MAX;
else
timeout = (int)(ts->tv_sec * 1000 +
(ts->tv_nsec + 999999) / 1000000);
} else
timeout = -1;
/* epoll does not work with zero events */
if (eloop->nfds == 0) {
n = ppoll(NULL, 0, ts, &eloop->sigset);
#ifdef HAVE_EPOLL_PWAIT2
} else if (!eloop->epoll_pwait2_nosys) {
/* Many linux distros are dumb in shipping newer
* kernel headers than the target kernel they are using.
* So we jump through this stupid hoop and have to write
* more complex code. */
n = epoll_pwait2(eloop->fd, eloop->fds, (int)eloop->nfds,
ts, &eloop->sigset);
if (n == -1 && errno == ENOSYS) {
eloop->epoll_pwait2_nosys = true;
goto epoll_pwait2_nosys;
}
#endif
} else {
int timeout;
n = epoll_pwait(eloop->fd, eloop->fds, (int)eloop->nfds, timeout,
&eloop->sigset);
#ifdef HAVE_EPOLL_PWAIT2
epoll_pwait2_nosys:
#endif
if (ts != NULL) {
if (ts->tv_sec > INT_MAX / 1000 ||
(ts->tv_sec == INT_MAX / 1000 &&
((ts->tv_nsec + 999999) / 1000000 >
INT_MAX % 1000000)))
timeout = INT_MAX;
else
timeout = (int)(ts->tv_sec * 1000 +
(ts->tv_nsec + 999999) / 1000000);
} else
timeout = -1;
n = epoll_pwait(eloop->fd, eloop->fds, (int)eloop->nfds,
timeout, &eloop->sigset);
}
if (n == -1)
return -1;

View File

@ -98,6 +98,7 @@ struct eloop *eloop_new_with_signals(struct eloop *);
void eloop_free(struct eloop *);
void eloop_exit(struct eloop *, int);
void eloop_exitall(int);
void eloop_exitallinners(int);
int eloop_forked(struct eloop *, unsigned short);
int eloop_start(struct eloop *);

View File

@ -448,10 +448,11 @@ ipv4ll_drop(struct interface *ifp)
assert(ifp != NULL);
eloop_timeout_delete(ifp->ctx->eloop, NULL, ifp);
ipv4ll_freearp(ifp);
if ((ifp->options->options & DHCPCD_NODROP) == DHCPCD_NODROP)
return;
goto free;
state = IPV4LL_STATE(ifp);
if (state) {
@ -481,6 +482,10 @@ ipv4ll_drop(struct interface *ifp)
rt_build(ifp->ctx, AF_INET);
script_runreason(ifp, "IPV4LL");
}
free:
ipv4ll_free(ifp);
dhcpcd_dropped(ifp);
}
void

View File

@ -1927,6 +1927,7 @@ ipv6nd_recvmsg(struct dhcpcd_ctx *ctx, struct msghdr *msg)
/* Don't do anything if the user hasn't configured it. */
if (ifp->active != IF_ACTIVE_USER ||
ifp->options->options & DHCPCD_STOPPING ||
!(ifp->options->options & DHCPCD_IPV6))
return;

View File

@ -325,6 +325,9 @@ static struct sock_filter ps_seccomp_filter[] = {
#ifdef __NR_epoll_pwait
SECCOMP_ALLOW(__NR_epoll_pwait),
#endif
#ifdef __NR_epoll_pwait2
SECCOMP_ALLOW(__NR_epoll_pwait2),
#endif
#ifdef __NR_exit_group
SECCOMP_ALLOW(__NR_exit_group),
#endif

View File

@ -760,7 +760,7 @@ ps_stopwait(struct dhcpcd_ctx *ctx)
#endif
error = eloop_start(ctx->ps_eloop);
if (error != EXIT_SUCCESS)
if (error < 0)
logerr("%s: eloop_start", __func__);
eloop_timeout_delete(ctx->ps_eloop, ps_process_timeout, ctx);
@ -1136,6 +1136,10 @@ ps_recvpsmsg(struct dhcpcd_ctx *ctx, int fd, unsigned short events,
struct msghdr msg = { .msg_iov = iov, .msg_iovlen = 1 };
bool stop = false;
if (events & ELE_HANGUP) {
len = 0;
goto stop;
}
if (!(events & ELE_READ))
logerrx("%s: unexpected event 0x%04x", __func__, events);
@ -1163,12 +1167,13 @@ ps_recvpsmsg(struct dhcpcd_ctx *ctx, int fd, unsigned short events,
}
if (stop) {
stop:
ctx->options |= DHCPCD_EXITING;
#ifdef PRIVSEP_DEBUG
logdebugx("process %d stopping", getpid());
#endif
ps_free(ctx);
eloop_exit(ctx->eloop, len != -1 ? EXIT_SUCCESS : EXIT_FAILURE);
eloop_exitall(len != -1 ? EXIT_SUCCESS : EXIT_FAILURE);
return len;
}
dlen -= sizeof(psm.psm_hdr);

View File

@ -824,7 +824,8 @@ rt_build(struct dhcpcd_ctx *ctx, int af)
}
#ifdef BSD
if (if_missfilter_apply(ctx) == -1 && errno != ENOTSUP)
if (!(ctx->options & DHCPCD_EXITING) &&
if_missfilter_apply(ctx) == -1 && errno != ENOTSUP)
logerr("if_missfilter_apply");
#endif