Your DHCP server is handing out addresses it never checked
Your DHCP server is handing out addresses it never checked
你的 DHCP 服务器正在分配未经检查的 IP 地址
Tested on Windows Server 2025 (24H2, build 26100.33296). A machine boots, gets a 169.254.x.x address, and has no network. The DHCP server is up. The service is running. There are no errors in the event log. There are free addresses in the scope. Reboot the client and it works fine. I first saw this on Windows Server 2003. I assumed it had been fixed somewhere along the way. It has not — because it was never a bug. It is a default, and the default is still there in Windows Server 2025. 在 Windows Server 2025 (24H2, build 26100.33296) 上进行测试:一台机器启动后获取到了 169.254.x.x 的地址,且无法联网。此时 DHCP 服务器处于运行状态,服务正常,事件日志中没有错误,地址池中也有可用地址。重启客户端后一切正常。我最早在 Windows Server 2003 上就发现了这个问题,当时以为后续版本会修复它。但事实并非如此——因为它从来就不是一个 Bug,而是一个默认设置,且这个默认设置在 Windows Server 2025 中依然存在。
What the RFC actually says
RFC 文档的实际规定
RFC 2131 splits address-conflict checking across both ends of the conversation: the allocating server SHOULD probe the reused address before allocating the address, e.g., with an ICMP echo request, and the client SHOULD probe the newly received address, e.g., with ARP. Two recommendations. Both SHOULD, neither MUST. Microsoft implemented both — but ships the server half turned off. The setting is called “Conflict detection attempts,” it lives under IPv4 → Properties → Advanced, and its default value is 0. RFC 2131 将地址冲突检查分散在通信的两端:分配服务器“应该 (SHOULD)”在分配地址前探测该地址是否已被占用(例如使用 ICMP 回显请求),客户端也“应该 (SHOULD)”在收到新地址后进行探测(例如使用 ARP)。这是两条建议,都是“应该”而非“必须”。微软实现了这两项功能,但默认将服务器端的检查功能关闭了。该设置名为“冲突检测尝试次数 (Conflict detection attempts)”,位于 IPv4 → 属性 → 高级选项卡下,默认值为 0。
The lab
实验环境
Three VMs on a VMware Workstation LAN Segment — a virtual switch with no uplink and no host interface, so nothing here can reach or disturb the physical network: 在 VMware Workstation 的 LAN 段上运行三台虚拟机——这是一个没有上行链路和主机接口的虚拟交换机,因此内部流量不会触及或干扰物理网络:
| Role | Address | MAC |
|---|---|---|
| DHCP server (Windows Server 2025) | 172.16.0.2 (static) | 00:0c:29:52:b9:9f |
| Squatter (Linux, static, never speaks DHCP) | 172.16.0.10 | 00:0c:29:3a:4c:11 |
| Client (Windows 10, WIN10-01) | DHCP | 00:0c:29:b8:ca:88 |
The scope is 172.16.0.0/24 with a deliberately tiny range, so that the address I want the server to pick is the only one it can pick. Lease duration: 8 days. The squatter simulates what you get from a rogue DHCP server or a hand-configured host inside the scope range: something holding an address the DHCP server believes is free. 地址池为 172.16.0.0/24,范围被刻意设得很小,使得服务器只能选择我指定的那个地址。租约期限为 8 天。“非法占用者 (Squatter)”模拟了来自非法 DHCP 服务器或手动配置的主机,它们占用了 DHCP 服务器认为空闲的地址。
Test 1 — the default (Conflict detection attempts = 0)
测试 1 — 默认设置(冲突检测尝试次数 = 0)
Scope range: 172.16.0.10 – 172.16.0.10. 地址池范围:172.16.0.10 – 172.16.0.10。
(Packet capture analysis omitted for brevity) (此处省略抓包分析数据)
The server handed out an occupied address 406 microseconds after being asked for one. You do not need to argue about whether it checked: there is no room for a check in 406 µs. The whole capture contains zero ICMP packets and not a single ARP frame originating from the server’s MAC. 服务器在收到请求后仅 406 微秒就分配了一个已被占用的地址。无需争论它是否进行了检查:在 406 微秒内根本没有时间进行检查。整个抓包记录中没有 ICMP 数据包,也没有任何来自服务器 MAC 地址的 ARP 帧。
The client caught it. It ARP-probed the address it had just been given, the squatter answered, and the client sent DHCPDECLINE 3.1 seconds after the ACK — then fell back to APIPA. 客户端发现了冲突。它对刚获取的地址进行了 ARP 探测,非法占用者做出了响应,客户端在收到 ACK 后 3.1 秒发送了 DHCPDECLINE,随后回退到 APIPA(自动专用 IP 寻址)。
This is the important part. The client-side safety net works, so you usually do not get a duplicate IP. You get something that is arguably worse to diagnose: a machine with no network at all, sitting next to a healthy DHCP server that has just told it everything is fine. 这是重点所在。客户端的安全网起作用了,所以你通常不会得到重复的 IP 地址。但你得到的结果可能更难排查:一台完全无法联网的机器,旁边却是一台刚刚告诉它“一切正常”的健康 DHCP 服务器。
Detection limitations
检测的局限性
Detection — on either side — only works if the occupant is awake and willing to reply at the exact moment it is asked. A host that is powered off, asleep, on a link that is momentarily down, or sitting behind a fabric that suppresses ARP will be scored as absent. The address is handed out, and the duplicate appears later, when that host comes back and starts answering for an address someone else is now using. 无论哪一端的检测,只有在占用者处于开机状态且愿意在被询问的瞬间做出响应时才有效。如果主机处于关机、睡眠、链路暂时中断或位于抑制 ARP 的网络架构后,它会被判定为“不存在”。地址会被分配出去,而当该主机恢复并开始响应其地址时,冲突就会在稍后出现。
What the decline costs you
拒绝分配的代价
The declined address goes into the lease table as BAD_ADDRESS. It is not a leak; it is a recurring tax. Each cycle costs one unlucky client a failed boot. The server does not store who claimed the conflict or who reported it. There is nothing on the server side to trace the conflict back to its source: you have to go to the switch or capture packets. 被拒绝的地址会在租约表中标记为 BAD_ADDRESS。这不是内存泄漏,而是一种“循环税”。每个周期都会导致一个倒霉的客户端启动失败。服务器不会存储是谁占用了地址或谁报告了冲突。在服务器端没有任何方法可以追溯冲突的来源:你必须去检查交换机或进行抓包分析。
Test 2 — Conflict detection attempts = 2
测试 2 — 冲突检测尝试次数 = 2
Set-DhcpServerv4Setting -ConflictDetectionAttempts 2. 执行命令:Set-DhcpServerv4Setting -ConflictDetectionAttempts 2。
The server probed 481 microseconds after the DISCOVER, found the address occupied, and refused to offer it. With only one address in the scope, it then had nothing to give: nine DISCOVERs across the capture, zero OFFERs. Correct behaviour, but not useful to a client. Which is why the scope needs a second address. 服务器在收到 DISCOVER 后 481 微秒进行了探测,发现地址已被占用,因此拒绝分配。由于地址池中只有一个地址,它随后无地址可分配:抓包显示 9 次 DISCOVER 请求,0 次 OFFER 响应。这是正确的行为,但对客户端没有帮助。这就是为什么地址池至少需要第二个地址的原因。