From 1d3adf93f936f53edc8c96d262f50fe4c62693c8 Mon Sep 17 00:00:00 2001 From: Colin McInnes Date: Tue, 1 Sep 2026 17:09:55 +0000 Subject: [PATCH 1/2] Support Force Renew with zero xid --- src/dhcp.c | 42 ++++++++++++++++++++++++++---------------- 1 file changed, 26 insertions(+), 16 deletions(-) diff --git a/src/dhcp.c b/src/dhcp.c index 078f6927..1c36258c 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -3128,15 +3128,35 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, return; } - if (state->xid != ntohl(bootp->xid)) { - if (IS_STATE_ACTIVE(state)) - logdebugx("%s: wrong xid 0x%x (expecting 0x%x) from %s", - ifp->name, ntohl(bootp->xid), state->xid, - inet_ntoa(*from)); - dhcp_redirect_dhcp(ifp, bootp, bootp_len, from); + /* We may have found a BOOTP server */ + if (get_option_uint8(ifp->ctx, &type, bootp, bootp_len, + DHO_MESSAGETYPE) == -1) + type = 0; + else if (ifo->options & DHCPCD_BOOTP) { + logdebugx("%s: ignoring DHCP reply (expecting BOOTP)", + ifp->name); return; } + if (state->xid != ntohl(bootp->xid)) { + /* If the xid is 0 in a BOOTP reply FORCERENEW, move on and + * check AUTH. MikroTik dhpc servers send xid 0. */ + if (bootp->xid == 0 && type == DHCP_FORCERENEW) { + if (IS_STATE_ACTIVE(state)) + logdebugx( + "%s: xid 0 in a BOOTP reply FORCERENEW from %s", + ifp->name, inet_ntoa(*from)); + } else { + if (IS_STATE_ACTIVE(state)) + logdebugx( + "%s: wrong xid 0x%x (expecting 0x%x) from %s", + ifp->name, ntohl(bootp->xid), state->xid, + inet_ntoa(*from)); + dhcp_redirect_dhcp(ifp, bootp, bootp_len, from); + return; + } + } + if (ifp->hwlen <= sizeof(bootp->chaddr) && memcmp(bootp->chaddr, ifp->hwaddr, ifp->hwlen)) { if (IS_STATE_ACTIVE(state)) { @@ -3170,16 +3190,6 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, } } - /* We may have found a BOOTP server */ - if (get_option_uint8(ifp->ctx, &type, bootp, bootp_len, - DHO_MESSAGETYPE) == -1) - type = 0; - else if (ifo->options & DHCPCD_BOOTP) { - logdebugx("%s: ignoring DHCP reply (expecting BOOTP)", - ifp->name); - return; - } - #ifdef AUTH /* Authenticate the message */ auth = get_option(ifp->ctx, bootp, bootp_len, DHO_AUTHENTICATION, From f570917dcebc66526b78cf7ffd2332966b7187ba Mon Sep 17 00:00:00 2001 From: Colin McInnes Date: Tue, 1 Sep 2026 15:55:39 -0600 Subject: [PATCH 2/2] DHCP: Authenticate Force Renew before xid check Require DHO_AUTHENTICATION for FORCERENEW before the xid-zero exception and defer the BOOTP-mode reject so authenticated reconfigure still runs. --- src/dhcp.c | 87 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 56 insertions(+), 31 deletions(-) diff --git a/src/dhcp.c b/src/dhcp.c index 1c36258c..8d73a908 100644 --- a/src/dhcp.c +++ b/src/dhcp.c @@ -3091,6 +3091,7 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, bool bootp_copied; uint32_t v6only_time = 0; bool use_v6only = false, has_auto_conf = false; + bool bootp_must_reject_dhcp = false; struct dhcp_policy dp = { .ctx = ifp->ctx, .bootp = bootp, @@ -3132,15 +3133,35 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, if (get_option_uint8(ifp->ctx, &type, bootp, bootp_len, DHO_MESSAGETYPE) == -1) type = 0; - else if (ifo->options & DHCPCD_BOOTP) { - logdebugx("%s: ignoring DHCP reply (expecting BOOTP)", - ifp->name); - return; + else + bootp_must_reject_dhcp = (ifo->options & DHCPCD_BOOTP) != 0; + +#ifdef AUTH + if (type == DHCP_FORCERENEW) { + auth = get_option(ifp->ctx, bootp, bootp_len, + DHO_AUTHENTICATION, &auth_len); + if (auth == NULL) { + LOGDHCP(LOG_ERR, "unauthenticated Force Renew"); + return; + } + if (dhcp_auth_validate(&state->auth, &ifo->auth, + (uint8_t *)bootp, bootp_len, 4, type, auth, + auth_len) == NULL) { + LOGDHCP0(LOG_ERR, "authentication failed"); + return; + } + if (state->auth.token) + logdebugx("%s: validated using 0x%08" PRIu32, ifp->name, + state->auth.token->secretid); + else + loginfox("%s: accepted reconfigure key", ifp->name); } +#endif if (state->xid != ntohl(bootp->xid)) { - /* If the xid is 0 in a BOOTP reply FORCERENEW, move on and - * check AUTH. MikroTik dhpc servers send xid 0. */ + /* AUTH has already run; this only skips redirect for + * MikroTik xid 0 FORCERENEW. Non-FORCERENEW xid + * mismatches still redirect. */ if (bootp->xid == 0 && type == DHCP_FORCERENEW) { if (IS_STATE_ACTIVE(state)) logdebugx( @@ -3191,27 +3212,30 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, } #ifdef AUTH - /* Authenticate the message */ - auth = get_option(ifp->ctx, bootp, bootp_len, DHO_AUTHENTICATION, - &auth_len); - if (auth) { - if (dhcp_auth_validate(&state->auth, &ifo->auth, - (uint8_t *)bootp, bootp_len, 4, type, auth, - auth_len) == NULL) { - LOGDHCP0(LOG_ERR, "authentication failed"); - return; - } - if (state->auth.token) - logdebugx("%s: validated using 0x%08" PRIu32, ifp->name, - state->auth.token->secretid); - else - loginfox("%s: accepted reconfigure key", ifp->name); - } else if (ifo->auth.options & DHCPCD_AUTH_SEND) { - if (ifo->auth.options & DHCPCD_AUTH_REQUIRE) { - LOGDHCP0(LOG_ERR, "no authentication"); - return; + if (type != DHCP_FORCERENEW) { + /* Authenticate the message */ + auth = get_option(ifp->ctx, bootp, bootp_len, + DHO_AUTHENTICATION, &auth_len); + if (auth) { + if (dhcp_auth_validate(&state->auth, &ifo->auth, + (uint8_t *)bootp, bootp_len, 4, type, auth, + auth_len) == NULL) { + LOGDHCP0(LOG_ERR, "authentication failed"); + return; + } + if (state->auth.token) + logdebugx("%s: validated using 0x%08" PRIu32, + ifp->name, state->auth.token->secretid); + else + loginfox("%s: accepted reconfigure key", + ifp->name); + } else if (ifo->auth.options & DHCPCD_AUTH_SEND) { + if (ifo->auth.options & DHCPCD_AUTH_REQUIRE) { + LOGDHCP0(LOG_ERR, "no authentication"); + return; + } + LOGDHCP0(LOG_WARNING, "no authentication"); } - LOGDHCP0(LOG_WARNING, "no authentication"); } #endif @@ -3223,11 +3247,6 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, return; } #ifdef AUTH - if (auth == NULL) { - LOGDHCP(LOG_ERR, "unauthenticated Force Renew"); - if (ifo->auth.options & DHCPCD_AUTH_REQUIRE) - return; - } if (state->state != DHS_BOUND && state->state != DHS_INFORM) { LOGDHCP(LOG_DEBUG, "not bound, ignoring Force Renew"); return; @@ -3247,6 +3266,12 @@ dhcp_handledhcp(struct interface *ifp, struct bootp *bootp, size_t bootp_len, return; } + if (bootp_must_reject_dhcp) { + logdebugx("%s: ignoring DHCP reply (expecting BOOTP)", + ifp->name); + return; + } + if (state->state == DHS_BOUND) { LOGDHCP(LOG_DEBUG, "bound, ignoring"); return;