DHCP八种报文全解析:Discover、Offer、Request、Ack 到 Nack、Decline、Release、Inform 的工作流程详解

发布时间:2026/10/18 1:17:18
DHCP八种报文全解析:Discover、Offer、Request、Ack 到 Nack、Decline、Release、Inform 的工作流程详解
1. DHCP 八种报文到底在干什么从抓包视角看完整交互流程DHCP 这八个报文名字你可能都背过但真到线上排障时能一眼从抓包文件里认出「这是 Decline 不是 Release」的人并不多。我先把结论摆出来DHCP 本质是一套「广播找房、房东报价、租客确认、房东交钥匙」的租约协议八种报文分别对应这套流程里的不同动作。Discover 是租客喊「谁有房」Offer 是房东说「我这有」Request 是租客说「我要这间」Ack 是房东交钥匙Nack 是房东说「这间不能给你」Decline 是租客发现钥匙打不开门Release 是退租Inform 是租客只想问物业信息不想换房。这套流程里最容易踩坑的地方在于很多人以为 Request 只在四步握手时出现一次实际上续约、重绑定、重启后确认旧地址用的都是 Request 报文只是目的不同。同样Ack 也不只出现在分配成功那一刻Inform 的回复也是 Ack。所以如果你只按「四步握手」去理解这八个报文遇到 Nack 和 Decline 就会懵。这篇文章面向网络运维和后端开发两类读者。运维关心的是「为什么这台机器拿不到地址」后端关心的是「我的服务在容器/虚机里重启后 IP 为什么变了」。两种问题最后都会落到同一件事上看懂 DHCP 报文的状态机。下面我会给出可复制的抓包过滤命令、状态机时序、异常排查清单以及在本地实验环境里亲手跑一遍 Discover→Offer→Request→Ack 的具体步骤。你不需要真实的路由器用 Linux 上的 dnsmasq 加一个 network namespace 就能复现。先明确端口约定这是抓包过滤的基础服务端用 UDP 67客户端用 UDP 68。所有 DHCP 报文都跑在这两个端口上所以过滤表达式基本围绕port 67 or port 68展开。记住这一点后面所有 tcpdump 命令都是从它衍生出来的。2. 用 TaoToken 辅助解析报文与排障脚本前置准备与接入配置排障过程中经常需要写点小脚本解析抓包、比对租约文件或者让模型帮你解释一段看不懂的 Option 字段。我平时会用 TaoToken 这类聚合入口来调用模型把抓包文本贴进去问「这个 Nack 的 Option 53 是什么含义」。它的官网是 https://taotoken.net/?utm_sourcetaotoken_aicg_blog_endutm_mediumcsdnutm_campaignrewriteutm_content API 入口是 https://taotoken.net/api 注意 API 地址不带 UTM 参数。如果你打算在脚本里调用模型做报文解释需要先拿到 Key。进入控制台的 API Keys 页面创建一个地址是 https://taotoken.net/console/api-keys?utm_sourcetaotoken_aicg_blog_endutm_contentapi-keysutm_campaignrewrite 。创建后复制那串 sk- 开头的字符串只显示一次丢了就得重建。拿到 Key 之后接入方式取决于你用什么工具。如果你用的是 Claude Code 这类命令行编码工具需要配置三件套Base URL、Key、Model ID。Base URL 填https://taotoken.net/apiKey 填刚创建的Model ID 按你选的模型填。对应的配置文档在 https://taotoken.net/doc?utm_sourcetaotoken_aicg_blog_endutm_contentdocutm_campaignrewrite 里面有各客户端的完整字段说明。对于写排障脚本这种场景我建议用 OpenAI 兼容的调用方式因为大多数语言的 SDK 都支持自定义 base_url。下面是一个 Python 示例把 tcpdump 抓到的 DHCP 报文文本丢给模型解释from openai import OpenAI client OpenAI( base_urlhttps://taotoken.net/api, api_keysk-你的Key ) pkt_text 12:00:01.123 IP 0.0.0.0.68 255.255.255.255.67: BOOTP/DHCP, Request Option 53: DHCP Message Type DHCP NAK Option 54: Server Identifier 192.168.1.1 resp client.chat.completions.create( model你的ModelID, messages[ {role: system, content: 你是网络协议分析助手用简洁中文解释DHCP报文含义。}, {role: user, content: f解释这段DHCP抓包重点说明为什么会出现NAK\n{pkt_text}} ] ) print(resp.choices[0].message.content)如果你更习惯在对话界面里手动贴报文可以直接用模型对话入口 https://taotoken.net/chat?utm_sourcetaotoken_aicg_blog_endutm_contentmodel-chatutm_campaignrewrite 把抓包文本粘进去问。对于长期要写网络自动化脚本、Agent 类工具的读者可以考虑 Coding Plan https://taotoken.net/coding-plan?utm_sourcetaotoken_aicg_blog_endutm_contentcoding-planutm_campaignrewrite 按订阅方式用会更省心。需要提醒的是TaoToken 在这里的角色是「帮你理解和生成排障脚本」不是替代你的抓包工具或 DHCP 服务器。真正的报文交互还是发生在你的网络设备之间模型只是加速你读懂它。3. 可复制的抓包过滤命令与状态机配置本地复现四步握手这一节是全文的核心操作部分。我会给出完整的抓包命令、dnsmasq 配置片段以及一个 network namespace 实验环境让你能在单台 Linux 机器上跑通 Discover→Offer→Request→Ack。先看抓包。最基础的过滤是抓所有 DHCP 流量sudo tcpdump -i any -n -vvv port 67 or port 68-i any抓所有接口-n不做 DNS 反解-vvv展开 Option 字段。如果你只想看某个 MAC 的交互加上 ether hostsudo tcpdump -i any -n -vvv port 67 or port 68 and ether host aa:bb:cc:dd:ee:ff想直接存成 pcap 用 Wireshark 分析sudo tcpdump -i any -n -w dhcp.pcap port 67 or port 68Wireshark 里的显示过滤器更直观常用的几个bootp.option.dhcp 1 # Discover bootp.option.dhcp 2 # Offer bootp.option.dhcp 3 # Request bootp.option.dhcp 5 # Ack bootp.option.dhcp 6 # Nack bootp.option.dhcp 4 # Decline bootp.option.dhcp 7 # Release bootp.option.dhcp 8 # InformOption 53 就是 Message Type 字段这是识别八种报文最直接的方式。记住这个映射比记报文名字更实用。接下来搭实验环境。用 network namespace 模拟一个客户端和一个服务端中间用 veth pair 连接# 创建服务端和客户端命名空间 sudo ip netns add dhcp-server sudo ip netns add dhcp-client # 创建 veth pair sudo ip link add veth-s type veth peer name veth-c sudo ip link set veth-s netns dhcp-server sudo ip link set veth-c netns dhcp-client # 配置服务端地址 sudo ip netns exec dhcp-server ip addr add 192.168.100.1/24 dev veth-s sudo ip netns exec dhcp-server ip link set veth-s up sudo ip netns exec dhcp-server ip link set lo up # 客户端接口起来但不配地址 sudo ip netns exec dhcp-client ip link set veth-c up sudo ip netns exec dhcp-client ip link set lo up服务端用 dnsmasq配置文件dhcpd.conf片段如下interfaceveth-s bind-interfaces dhcp-range192.168.100.100,192.168.100.150,255.255.255.0,12h dhcp-option3,192.168.100.1 dhcp-option6,8.8.8.8 log-dhcp启动 dnsmasqsudo ip netns exec dhcp-server dnsmasq -C dhcpd.conf -d-d让它在前台运行并打印日志你能直接看到 Offer、Ack 的发送记录。然后在另一个终端启动抓包再让客户端发起请求sudo ip netns exec dhcp-client dhclient -v veth-c-v会打印详细过程。此时你应该能在 dnsmasq 日志里看到DHCPDISCOVER、DHCPOFFER、DHCPREQUEST、DHCPACK四条记录抓包文件里也能看到对应的四个报文。这就是完整的四步握手。关于状态机客户端侧的状态迁移是这样的INIT 状态发 Discover收到 Offer 进入 SELECTING发 Request收到 Ack 进入 BOUND。BOUND 状态下租期过半T1进入 RENEWING发单播 Request如果 T1 没收到 Ack到 87.5%T2进入 REBINDING发广播 Request再失败就回到 INIT。这个 T1/T2 的时间点是排查「续约失败」类问题的关键。服务端侧收到 Request 后会根据报文里的 server identifier 和 requested IP 判断客户端处于哪个状态SELECTING 状态校验 server id 是否匹配INIT-REBOOT 状态校验 requested IP 是否符合租约RENEWING/REBINDING 状态校验 ciaddr 是否符合租约。任何一项校验失败回的就是 Nack。4. 验证请求与成功结果从抓包确认八种报文真实出现光配好还不够得亲眼确认每种报文长什么样。这一节我给出验证方法和预期输出你照着比对就行。先验证四步握手。启动抓包后执行 dhclient抓包输出里应该依次出现0.0.0.0.68 255.255.255.255.67: BOOTP/DHCP, Request, length 300 Option 53: DHCP Message Type DHCP Discover Option 55: Parameter Request List Option 61: Client Identifier 192.168.100.1.67 255.255.255.255.68: BOOTP/DHCP, Reply, length 300 Option 53: DHCP Message Type DHCP Offer Option 54: Server Identifier 192.168.100.1 Your-IP 192.168.100.100 0.0.0.0.68 255.255.255.255.67: BOOTP/DHCP, Request, length 300 Option 53: DHCP Message Type DHCP Request Option 50: Requested-IP Address 192.168.100.100 Option 54: Server Identifier 192.168.100.1 192.168.100.1.67 255.255.255.255.68: BOOTP/DHCP, Reply, length 300 Option 53: DHCP Message Type DHCP Ack Your-IP 192.168.100.100 Option 51: IP Address Lease Time 43200注意 Discover 和 Request 的源地址都是 0.0.0.0因为客户端此时还没有 IP。Offer 和 Ack 里Your-IP字段yiaddr才是分配给你的地址。这个字段只在 Offer 和 Ack 里非零其他报文里都是 0这是快速识别报文类型的一个技巧。验证续约。租期设短一点比如 120 秒然后在客户端跑sudo ip netns exec dhcp-client dhclient -v veth-c # 等 60 秒左右观察T1 时刻你会看到一条单播 Request源地址是客户端已获得的 IP目的地址是服务器 IP不再是广播。这就是 RENEWING 状态的报文特征。如果 T1 没成功T2 时刻会看到广播 Request源地址仍是客户端 IP目的地址是 255.255.255.255。验证 Release。手动释放sudo ip netns exec dhcp-client dhclient -r veth-c抓包里会出现一条单播 Release源地址是客户端 IP目的地址是服务器 IP。dnsmasq 日志里会显示释放了哪个地址。验证 Decline 比较麻烦需要人为制造地址冲突。你可以在服务端地址池里先手动占一个地址比如在 dhcp-server 命名空间里给某个接口配上 192.168.100.100然后让客户端去申请。客户端拿到 Offer 后做 ARP 冲突检测发现地址已被占用就会发 Decline。抓包里能看到Option 53: DHCP Message Type DHCP Decline Option 50: Requested-IP Address 192.168.100.100 Option 54: Server Identifier 192.168.100.1服务端收到 Decline 后会把该地址标记为不可用一段时间日志里通常显示DHCPDECLINE。验证 Inform。这个报文用于客户端已有 IP、只想获取额外配置比如 DNS、NTP的场景。可以用 dhclient 的-I参数或直接构造实际运维中更多出现在 PXE 或特定嵌入式设备上。抓包里它的特征是 ciaddr 非零客户端已有地址且没有 requested IP。验证 Nack。最简单的触发方式是让客户端请求一个不在地址池里的 IP。你可以修改客户端请求的地址或者让服务端地址池缩小到不包含客户端上次的地址。抓包里 Nack 的特征是 Option 53 值为 6且通常带一个 message 字段说明原因。跑完这一圈八种报文你至少亲眼见过六种剩下 Inform 和 Nack 在特定场景才会出现理解它们的触发条件就够了。5. 本篇常见错误排查清单401、local proxy failed、reading choices 与 OAuth这一节把两类问题分开讲一类是 DHCP 协议本身的异常一类是你在用模型辅助排障时可能遇到的接入报错。先说 DHCP 侧的异常清单。遇到客户端拿不到地址按这个顺序查第一看有没有 Discover 发出。如果抓包里连 Discover 都没有问题在客户端网卡或 dhclient 没启动跟服务端无关。检查ip link确认接口 up检查 dhclient 进程是否存在。第二有 Discover 没 Offer。说明服务端没响应。检查 dnsmasq 是否在监听正确接口bind-interfaces配了吗检查地址池是否耗尽检查服务端防火墙是否放行 UDP 67。dnsmasq 日志里如果连DHCPDISCOVER都没打印就是包没到。第三有 Offer 没 Request。客户端收到 Offer 后不发 Request通常是客户端在做地址冲突检测时发现地址被占直接发了 Decline。这时候抓包里应该能看到 Decline服务端日志会显示地址被标记。排查方向是地址池里是否有残留的静态配置或 ARP 冲突。第四有 Request 没 Ack反而收到 Nack。这就是前面说的校验失败。常见原因客户端请求的 IP 不在地址池范围、请求的 IP 已被静态租约占用、server identifier 不匹配多 DHCP 服务器环境。看 Nack 里的 message 字段dnsmasq 通常会写明原因。第五续约失败。T1 单播 Request 没回应检查客户端到服务器的单播路由是否通。跨网段场景下单播续约需要中继支持如果中继没配好单播包到不了服务器就会退化到 T2 广播。T2 广播还失败租期到 100% 客户端就会放弃地址回落到 169.254 网段。再说模型接入侧的报错。如果你在脚本里调用 TaoToken 时遇到 401基本是 Key 不对或没带。检查api_key字段是否填了完整的 sk- 字符串检查 base_url 是否写成了https://taotoken.net/api注意不要多加路径。401 的返回体里通常有invalid_api_key之类的提示。遇到local proxy failed这类报错通常是本地网络环境或客户端配置问题检查你的 HTTP 客户端是否设置了不必要的代理或者 base_url 是否被错误地指向了本地地址。这类报错跟 Key 无关重点查网络配置。遇到reading choices相关的报错一般是响应体解析失败。可能原因模型 ID 填错导致返回了非预期结构或者流式/非流式参数不匹配。检查model字段是否是你账号下可用的模型检查是否误开了 stream 但按非流式解析。遇到 OAuth 相关报错说明你用的客户端走的是 OAuth 授权流程而不是 API Key。Claude Code 这类工具如果配置成 OAuth 模式就不会用你填的 Key。需要在配置里明确指定用 API Key 方式把 Base URL、Key、Model ID 三件套填全。这三件套缺一不可Base URL 决定请求发到哪Key 决定身份Model ID 决定用哪个模型。任何一项缺失或错误都会导致请求失败。排查时建议先用最简单的 curl 验证 Key 是否可用curl https://taotoken.net/api/v1/chat/completions \ -H Authorization: Bearer sk-你的Key \ -H Content-Type: application/json \ -d {model:你的ModelID,messages:[{role:user,content:ping}]}这条通了再往客户端里配。客户端报错时先确认 curl 能通能快速区分是 Key 问题还是客户端配置问题。6. 把八种报文用起来从抓包到自动化排障的落地建议学完这八种报文最有价值的不是背下来而是把它变成你排障时的条件反射。我给你几个落地建议。第一把 Option 53 的映射表存成速查片段抓包时直接对照。1 到 8 分别对应 Discover、Offer、Request、Decline、Ack、Nack、Release、Inform。这个顺序跟报文出现的常见顺序不完全一致但数字是固定的记数字比记名字可靠。第二写一个简单的抓包分析脚本把 pcap 里的 DHCP 报文按 xid 分组还原出每次交互的完整流程。同一个 xid 的报文属于同一次请求这样你能一眼看出某次交互卡在哪一步。这个脚本用 Python 的 scapy 或 dpkt 都能写配合前面说的模型解释效率会高很多。第三在跨网段环境里特别关注 giaddr 字段。如果 giaddr 非零说明报文经过了中继此时单播和广播的行为会跟同网段不同。很多「续约失败」的根因就是中继没正确转发单播 Request。第四Decline 和 Nack 要区别对待。Decline 是客户端主动拒绝说明地址冲突Nack 是服务端拒绝说明租约校验失败。两者都会导致客户端重新发起 Discover但排查方向完全不同。看到 Decline 查地址冲突看到 Nack 查租约和地址池配置。第五租期设置要结合场景。办公网可以设长一点减少续约流量但地址池紧张时设短一点加快回收。T1 和 T2 的默认值是租期的 50% 和 87.5%这个比例一般不用改但你要知道它们的存在才能在续约失败时判断客户端处于哪个阶段。最后说一句实操心得本地用 network namespace 加 dnsmasq 复现这套流程比在真实网络里试错安全得多也快得多。把四步握手跑通一次再故意制造一次 Decline 和一次 Nack你对这八种报文的理解就从「背过」变成「用过」了。真到线上出问题时抓包文件里的 Option 53 会直接告诉你答案。