<html><body><div>Hi folks,<br></div><div><br></div><div>I think I've found a regression in dnsmasq 2.92 that breaks PXE boot server</div><div>(PXEBS) responses when running in proxy DHCP mode. Fair warning: I'm not</div><div>familiar with the dnsmasq codebase and used AI tooling to help trace through</div><div>the source and identify the issue, so please take the analysis below with</div><div>appropriate skepticism. PXE boot works fine on 2.91</div><div>but fails on 2.92 — the client gets the initial proxy DHCPOFFER, but the PXEBS</div><div>ACK on port 4011 never reaches it.</div><div><br></div><div>My setup is dnsmasq in proxy DHCP mode serving iPXE to Proxmox VMs via their</div><div>virtio-net ROM. Here's a stripped-down version of my config:</div><div><br></div><div> port=0</div><div> enable-tftp</div><div> tftp-root=/tftpboot</div><div> dhcp-range=172.19.74.0,proxy,255.255.255.0</div><div> interface=eno1</div><div> bind-interfaces</div><div> dhcp-match=set:ipxe,175</div><div> pxe-service=tag:ipxe,x86PC,"Network Boot",<a href="http://server:8081/boot.ipxe">http://server:8081/boot.ipxe</a></div><div> pxe-service=tag:!ipxe,x86PC,"Network Boot",undionly.kpxe</div><div> log-dhcp</div><div><br></div><div>The issue seems to be in src/dhcp.c in the response routing logic after</div><div>dhcp_reply() returns. In 2.91, the destination selection was an if/else-if</div><div>chain:</div><div><br></div><div> if (pxe_fd)</div><div> { ... }</div><div> else if (mess->giaddr.s_addr && !is_relay_reply)</div><div> { ... }</div><div> else if (mess->ciaddr.s_addr)</div><div> { ... }</div><div> else</div><div> { ... broadcast to <a href="http://255.255.255.255:68">255.255.255.255:68</a> ... }</div><div><br></div><div>In 2.92, the else between the pxe_fd block and the giaddr/relay check was</div><div>removed in commit 4fbe1ad ("Implement RFC-4388 DHCPv4 leasequery") to</div><div>accommodate the new is_relay_use_source logic:</div><div><br></div><div> if (pxe_fd)</div><div> { ... }</div><div> if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply)</div><div> { ... }</div><div> else if (mess->ciaddr.s_addr)</div><div> { ... }</div><div> else</div><div> { ... broadcast to <a href="http://255.255.255.255:68">255.255.255.255:68</a> ... }</div><div><br></div><div>For PXEBS responses, dhcp_reply() in rfc2131.c (around line 924-925) does:</div><div><br></div><div> mess->yiaddr = mess->ciaddr;</div><div> mess->ciaddr.s_addr = 0;</div><div><br></div><div>So after dhcp_reply() returns for a PXEBS request, ciaddr is 0, giaddr is 0</div><div>(no relay), and is_relay_use_source is 0. In 2.91, the pxe_fd block runs and</div><div>the rest of the chain is skipped — dest stays as received from recvmsg, and the</div><div>response goes back to the client correctly. In 2.92, the pxe_fd block runs but</div><div>then falls through to the standalone if, which is false, so the else block runs</div><div>and sets dest to 255.255.255.255 port 68. The client is listening on port 4011</div><div>and ignores it.</div><div><br></div><div>Here are the relevant dnsmasq logs. With 2.91 (working), I see normal proxy</div><div>DHCP and PXE boot server exchanges:</div><div><br></div><div> dnsmasq-dhcp: DHCPDISCOVER(eno1) bc:24:11:59:85:90</div><div> dnsmasq-dhcp: DHCPOFFER(eno1) 172.19.74.60 bc:24:11:59:85:90</div><div> dnsmasq-dhcp: DHCPREQUEST(eno1) 172.19.74.60 bc:24:11:59:85:90</div><div> dnsmasq-dhcp: DHCPACK(eno1) 172.19.74.60 bc:24:11:59:85:90</div><div> dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy</div><div> dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe</div><div> dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 <a href="http://infra1.oneill.net:8081/boot.ipxe">http://infra1.oneill.net:8081/boot.ipxe</a></div><div><br></div><div>With 2.92 (broken), the DHCPDISCOVER/OFFER/REQUEST/ACK cycle and the proxy</div><div>PXE response work, but the PXEBS response never reaches the client — it times</div><div>out after repeated attempts. The dnsmasq side shows it sending the response,</div><div>but the client keeps retrying:</div><div><br></div><div> dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy</div><div> dnsmasq-dhcp: PXE(eno1) bc:24:11:59:85:90 proxy</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe</div><div> dnsmasq-dhcp: PXEBS(eno1) bc:24:11:59:85:90 undionly.kpxe</div><div><br></div><div>I tested by restoring the else keyword and the fix appears to work — 2.92 with</div><div>the patch below PXE boots successfully. I believe this change preserves the</div><div>leasequery behavior since that path only applies when pxe_fd is false (normal</div><div>DHCP handling, not port 4011).</div><div><br></div><div>--- a/src/dhcp.c</div><div>+++ b/src/dhcp.c</div><div>@@ -399,7 +399,7 @@ void dhcp_packet(time_t now, int pxe_fd)</div><div> if (mess->ciaddr.s_addr != 0)</div><div> <span class="Apple-tab-span" style="white-space:pre"> </span>dest.sin_addr = mess->ciaddr;</div><div> }</div><div>- if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply)</div><div>+ else if ((is_relay_use_source || mess->giaddr.s_addr) && !is_relay_reply)</div><div> {</div><div> /* Send to BOOTP relay. */</div><div> if (is_relay_use_source)</div><div><br></div><div>Thanks,</div><div>Clayton</div><div><br></div></body></html>