TCP/IP卷1学习: 6.2.4 DHCP 协议操作详解
一、DHCP 是什么?
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议)的本质是:让新加入网络的设备,自动获得 IP 地址和网络配置,而无需手动设置。
DHCP 消息本质上是 BOOTP 消息加上一组特殊选项。客户端把 BOOTP 的 Op 字段设置为 BOOTREQUEST,并将 Options 字段的前 4 字节设置为十进制值
99
,
130
,
83
,
99
99, 130, 83, 99
99,130,83,99(即"魔法 Cookie",来自 RFC2132)。
二、标准四步握手(DORA 流程)
这是最经典的 DHCP 交互,如图所示:
客户端 服务器
| |
|------- DISCOVER (广播) ---------->| 客户端广播,寻找服务器
|<------ OFFER (广播) -------------| 服务器提供可用地址
|------- REQUEST (广播) ---------->| 客户端请求该地址
|<------ ACK (广播) --------------| 服务器确认,分配完成
| |
|--- Check for Conflict (ARP/ACD)->| 客户端验证地址无冲突
|--- DECLINE (如有冲突) ---------->| 若冲突则拒绝
步骤 1:DISCOVER(发现)
- 客户端刚接入网络,还没有 IP 地址,因此使用源地址 0.0.0.0 0.0.0.0 0.0.0.0(端口 68)向广播地址 255.255.255.255 255.255.255.255 255.255.255.255(端口 67)发送 UDP 广播。
- 消息中包含:事务ID(
xid,用于匹配请求与响应)、请求的参数列表。 - 所有网络内的 DHCP 服务器都会收到这条消息。
步骤 2:OFFER(提供)
- 每台收到 DISCOVER 的服务器都可以回复一个 OFFER,提供它愿意分配的 IP 地址。
- 消息中包含:服务器地址(
siaddr)、事务ID(xid)、为客户端提供的地址(yiaddr)、各种选项(如子网掩码、网关、DNS 等)。 - 客户端收集所有 OFFER,然后选择一个服务器和地址。
步骤 3:REQUEST(请求)
- 客户端选好之后,再次广播 REQUEST 消息(而非单播),这样:
- 被选中的服务器知道自己中选了;
- 其他服务器也能看到,从而释放它们预留的地址资源。
- 消息中包含:服务器地址(
siaddr)、客户端地址(ciaddr)、事务ID(xid)、选项。
步骤 4:ACK(确认)
- 被选中的服务器将地址绑定信息写入持久存储,其余服务器清除相关状态。
- 服务器发送 DHCPACK,客户端正式获得配置。
- 若服务器此时已无法提供该地址(被其他人抢先用了),则发送 DHCPNAK 拒绝请求。
三、地址冲突检测(ACD)
客户端收到 ACK 后,推荐但非强制地进行冲突检测:
客户端收到 ACK
|
v
发送 ARP 请求(询问"谁在用这个 IP?")
|
有人回应?
/ \
是(冲突) 否(无冲突)
| |
发 DECLINE 正常使用该地址
|
等待 10 秒后重试
- 使用的技术:ARP/ACD(Address Conflict Detection,地址冲突检测,见第4章)。
- 若发现冲突,客户端停止使用该地址,向服务器发送 DHCPDECLINE。
四、地址续租(Renew)
如果客户端已有 IP 地址,只想续租,可以跳过 DISCOVER 和 OFFER,直接从 REQUEST 开始:
五、DHCPINFORM(仅获取配置,不要地址)
当客户端已有 IP 地址,但需要获取其他配置信息(如 DNS 服务器地址)时,使用 DHCPINFORM:
客户端 ---DHCPINFORM(我已有地址,只需其他配置)---> 服务器
客户端 <--DHCPACK(返回所需配置信息)-------------- 服务器
六、消息类型总结
| 消息类型 | 发送方 | 含义 |
|---|---|---|
| DHCPDISCOVER | 客户端 | 广播寻找服务器 |
| DHCPOFFER | 服务器 | 提供可用地址 |
| DHCPREQUEST | 客户端 | 请求某地址/续租 |
| DHCPACK | 服务器 | 确认分配/续租 |
| DHCPNAK | 服务器 | 拒绝请求 |
| DHCPDECLINE | 客户端 | 地址冲突,拒绝使用 |
| DHCPRELEASE | 客户端 | 主动归还地址 |
| DHCPINFORM | 客户端 | 仅请求配置,不要地址 |
七、事务 ID(xid)的作用
由于所有消息都通过广播发送,网络中可能同时有多个 DHCP 交互在进行。事务ID
xid
\text{xid}
xid 是一个随机数,由客户端在 DISCOVER 中生成,所有后续消息都携带相同的 xid,用于将请求与响应对应起来。
匹配规则:
request.xid
=
response.xid
\text{匹配规则:} \quad \text{request.xid} = \text{response.xid}
匹配规则:request.xid=response.xid
八、完整 C++ 模拟示例(演示 DORA 流程逻辑)
#include <iostream>
#include <string>
#include <map>
#include <cstdlib>
#include <ctime>
// ===== 消息类型定义 =====
enum DHCPMessageType {
DHCPDISCOVER = 1,
DHCPOFFER = 2,
DHCPREQUEST = 3,
DHCPACK = 5,
DHCPNAK = 6,
DHCPDECLINE = 4,
DHCPRELEASE = 7,
DHCPINFORM = 8
};
// ===== DHCP 消息结构 =====
struct DHCPMessage {
DHCPMessageType type; // 消息类型
uint32_t xid; // 事务ID,用于匹配请求与响应
std::string ciaddr; // 客户端当前 IP(没有则为 "0.0.0.0")
std::string yiaddr; // 服务器为客户端提供的 IP
std::string siaddr; // 服务器自身 IP
std::string serverID; // 服务器标识(Option 54)
};
// ===== DHCP 服务器模拟 =====
class DHCPServer {
public:
std::string serverIP;
// 地址池:IP -> 是否已分配
std::map<std::string, bool> addressPool;
// 已绑定:xid -> 分配的IP(模拟预留)
std::map<uint32_t, std::string> pendingOffers;
DHCPServer(const std::string& ip) : serverIP(ip) {
// 初始化地址池(192.168.1.100 ~ 192.168.1.105)
for (int i = 100; i <= 105; ++i) {
addressPool["192.168.1." + std::to_string(i)] = false; // false = 未分配
}
}
// 处理 DISCOVER,返回 OFFER
DHCPMessage handleDiscover(const DHCPMessage& discover) {
std::cout << "[服务器 " << serverIP << "] 收到 DISCOVER,xid=" << discover.xid << "\n";
// 找一个空闲地址
std::string offeredIP = "";
for (auto& [ip, used] : addressPool) {
if (!used) {
offeredIP = ip;
break;
}
}
DHCPMessage offer;
offer.type = DHCPOFFER;
offer.xid = discover.xid; // 保持相同 xid
offer.yiaddr = offeredIP; // 提供的 IP
offer.siaddr = serverIP;
offer.serverID = serverIP;
if (!offeredIP.empty()) {
pendingOffers[discover.xid] = offeredIP; // 预留该地址
std::cout << "[服务器 " << serverIP << "] 发送 OFFER,提供 IP=" << offeredIP << "\n";
} else {
std::cout << "[服务器 " << serverIP << "] 地址池已满,无法提供地址\n";
}
return offer;
}
// 处理 REQUEST,返回 ACK 或 NAK
DHCPMessage handleRequest(const DHCPMessage& request) {
std::cout << "[服务器 " << serverIP << "] 收到 REQUEST,xid=" << request.xid
<< ",请求 IP=" << request.yiaddr << "\n";
DHCPMessage response;
response.xid = request.xid;
response.siaddr = serverIP;
response.serverID = serverIP;
// 检查该 xid 的预留是否还有效,且 IP 未被分配
if (pendingOffers.count(request.xid) &&
pendingOffers[request.xid] == request.yiaddr &&
!addressPool[request.yiaddr]) {
// 正式分配,写入持久存储(这里用 map 模拟)
addressPool[request.yiaddr] = true;
pendingOffers.erase(request.xid);
response.type = DHCPACK;
response.yiaddr = request.yiaddr;
std::cout << "[服务器 " << serverIP << "] 发送 ACK,确认分配 IP=" << request.yiaddr << "\n";
} else {
response.type = DHCPNAK;
std::cout << "[服务器 " << serverIP << "] 发送 NAK,地址不可用\n";
}
return response;
}
// 处理 DECLINE(客户端检测到地址冲突)
void handleDecline(const DHCPMessage& decline) {
std::cout << "[服务器 " << serverIP << "] 收到 DECLINE,地址 "
<< decline.yiaddr << " 存在冲突,标记为不可用\n";
// 将该地址从池中移除或标记为问题地址
addressPool.erase(decline.yiaddr);
}
};
// ===== DHCP 客户端模拟 =====
class DHCPClient {
public:
std::string clientID;
std::string assignedIP;
DHCPClient(const std::string& id) : clientID(id) {}
// 生成随机事务 ID
uint32_t generateXID() {
return (uint32_t)rand();
}
// 模拟 ARP 冲突检测(这里随机模拟,10% 概率检测到冲突)
bool detectConflict(const std::string& ip) {
std::cout << "[客户端] 发送 ARP 探测,检查 " << ip << " 是否已被使用...\n";
bool conflict = (rand() % 10 == 0); // 10% 概率冲突(模拟用)
if (conflict) {
std::cout << "[客户端] 检测到地址冲突!\n";
} else {
std::cout << "[客户端] 无冲突,地址可用。\n";
}
return conflict;
}
// 执行完整的 DORA 流程
void performDORA(DHCPServer& server) {
uint32_t xid = generateXID();
std::cout << "\n===== 开始 DHCP DORA 流程 =====\n";
std::cout << "[客户端 " << clientID << "] 事务 ID(xid)=" << xid << "\n\n";
// ---- Step 1: DISCOVER ----
std::cout << "--- Step 1: DISCOVER ---\n";
DHCPMessage discover;
discover.type = DHCPDISCOVER;
discover.xid = xid;
discover.ciaddr = "0.0.0.0"; // 还没有 IP
// ---- Step 2: OFFER ----
std::cout << "--- Step 2: OFFER ---\n";
DHCPMessage offer = server.handleDiscover(discover);
if (offer.yiaddr.empty()) {
std::cout << "[客户端] 没有收到有效 OFFER,流程终止。\n";
return;
}
std::cout << "[客户端] 收到 OFFER,服务器提供 IP=" << offer.yiaddr << "\n\n";
// ---- Step 3: REQUEST ----
std::cout << "--- Step 3: REQUEST ---\n";
DHCPMessage request;
request.type = DHCPREQUEST;
request.xid = xid;
request.yiaddr = offer.yiaddr; // 请求这个地址
request.siaddr = offer.siaddr;
request.serverID = offer.serverID;
std::cout << "[客户端] 广播 REQUEST,请求 IP=" << request.yiaddr << "\n";
// ---- Step 4: ACK ----
std::cout << "--- Step 4: ACK ---\n";
DHCPMessage ack = server.handleRequest(request);
if (ack.type == DHCPACK) {
std::cout << "[客户端] 收到 ACK,准备使用 IP=" << ack.yiaddr << "\n\n";
// ---- 冲突检测(推荐步骤)----
std::cout << "--- 冲突检测(ARP/ACD)---\n";
if (detectConflict(ack.yiaddr)) {
// 有冲突,发送 DECLINE
DHCPMessage decline;
decline.type = DHCPDECLINE;
decline.xid = xid;
decline.yiaddr = ack.yiaddr;
server.handleDecline(decline);
std::cout << "[客户端] 等待 10 秒后重试...\n";
} else {
// 无冲突,正式使用
assignedIP = ack.yiaddr;
std::cout << "[客户端 " << clientID << "] 成功配置,IP 地址=" << assignedIP << "\n";
}
} else {
std::cout << "[客户端] 收到 NAK,需重新发起 DISCOVER。\n";
}
std::cout << "===== DORA 流程结束 =====\n";
}
};
// ===== 主函数 =====
int main() {
srand((unsigned)time(nullptr));
// 创建一台 DHCP 服务器
DHCPServer server("192.168.1.1");
// 创建两台客户端,分别发起 DORA
DHCPClient client1("AA:BB:CC:DD:EE:01");
DHCPClient client2("AA:BB:CC:DD:EE:02");
client1.performDORA(server);
std::cout << "\n";
client2.performDORA(server);
return 0;
}
https://godbolt.org/z/nTarhsshz
编译运行方式:
g++ -std=c++17 -o dhcp_sim dhcp_sim.cpp && ./dhcp_sim
九、关键字段总结
| 字段 | 含义 |
|---|---|
xid | 事务ID,客户端随机生成,用于匹配请求与响应 |
ciaddr | 客户端当前 IP(若无则为 0.0.0.0 0.0.0.0 0.0.0.0) |
yiaddr | 服务器为客户端分配的 IP(“your IP”) |
siaddr | 服务器自身 IP(“server IP”) |
serverID | Option 54,标明哪台服务器在提供服务,用于多服务器场景下的区分 |
十、消息传输地址总结
客户端
→
服务器:
0.0.0.0
⏟
源地址
:
68
⏟
源端口
→
255.255.255.255
⏟
广播地址
:
67
⏟
目标端口
\text{客户端} \to \text{服务器:} \quad \underbrace{0.0.0.0}_{\text{源地址}}:\underbrace{68}_{\text{源端口}} \to \underbrace{255.255.255.255}_{\text{广播地址}}:\underbrace{67}_{\text{目标端口}}
客户端→服务器:源地址
0.0.0.0:源端口
68→广播地址
255.255.255.255:目标端口
67
服务器
→
客户端:
单播或广播,端口
68
\text{服务器} \to \text{客户端:} \quad \text{单播或广播,端口 } 68
服务器→客户端:单播或广播,端口 68
6.2.4.1 DHCP 实际抓包案例详解
场景:一台 Windows Vista 笔记本从旧无线网络切换到新无线网络,通过 Wireshark 抓包观察完整的 DHCP 交互过程。
一、整体流程概览
共抓取到 6 个 DHCP 数据包,完整时序如下:
No. Time 协议 源地址 目标地址 消息类型 事务ID
1 0.000000 DHCP 0.0.0.0 255.255.255.255 DHCP Request 0xdb23147d ← 客户端尝试用旧地址
2 0.018650 DHCP 10.0.0.1 255.255.255.255 DHCP NAK 0xdb23147d ← 服务器拒绝
3 1.083053 DHCP 0.0.0.0 255.255.255.255 DHCP Discover 0x3a681b0b ← 重新发现(新xid)
4 4.084315 DHCP 10.0.0.1 255.255.255.255 DHCP Offer 0x3a681b0b ← 服务器提供地址
5 4.087406 DHCP 0.0.0.0 255.255.255.255 DHCP Request 0x3a681b0b ← 客户端请求
6 4.104592 DHCP 10.0.0.1 255.255.255.255 DHCP ACK 0x3a681b0b ← 服务器确认
关键观察:
- 包 1~2 的 xid 都是
0xdb23147d,属于同一次失败的交互。 - 包 3~6 的 xid 都是
0x3a681b0b,属于重新发起的成功交互。 - xid(事务ID)的作用:
xid
\text{xid}
xid 是一个客户端随机生成的 32 位整数,用于将请求与响应一一对应。
完整流程的 mermaid 时序图:
二、包1详解:DHCPREQUEST(图6-3)
场景说明
客户端刚换了一个新的无线网络,但它记得自己在旧网络用的是 IP 地址 172.16.1.34。
按照 DHCP 协议规定,客户端可以先跳过 DISCOVER,直接用 REQUEST 尝试申请这个旧地址。
数据包分层结构
链路层:
源 MAC: 00:13:02:20:b9:18
目标MAC: ff:ff:ff:ff:ff:ff ← 链路层广播,整个局域网都能收到
网络层(IP):
源 IP: 0.0.0.0 ← 客户端还没有 IP,只能用全零地址
目标IP: 255.255.255.255 ← 受限广播地址
传输层(UDP):
源端口: 68 (BOOTP 客户端端口,bootpc)
目标端口: 67 (BOOTP 服务器端口,bootps)
应用层(Bootstrap Protocol / DHCP):
消息类型: Boot Request (1) ← 客户端 → 服务器
硬件类型: Ethernet (1)
MAC地址长度: 6 字节
Hops: 0 ← 没有经过中继代理转发
Transaction ID: 0xdb23147d ← 随机生成的事务ID
广播标志: 0x8000 ← 要求服务器用广播方式回复
ciaddr: 0.0.0.0 ← 客户端当前 IP(无)
yiaddr: 0.0.0.0 ← 服务器为客户端分配的 IP(待填)
siaddr: 0.0.0.0 ← 服务器 IP(待填)
chaddr: 00:13:02:20:b9:18 ← 客户端 MAC 地址
Magic Cookie: DHCP ← 魔法Cookie,值为 99.130.83.99
DHCP Options(选项):
Option 53 (t=53,l=1): DHCP Message Type = DHCP Request
Option 61 (t=61,l=7): Client Identifier = 0100130220B918
Option 50 (t=50,l=4): Requested IP Address = 172.16.1.34
Option 12 (t=12,l=5): Host Name = "vista"
Option 81: Client FQDN(全限定域名)
Option 60 (t=60,l=8): Vendor Class Identifier = "MSFT 5.0"
Option 55 (t=55,l=12): Parameter Request List
关键字段解读
Magic Cookie(魔法 Cookie)
这是区分 DHCP 和纯 BOOTP 的标志,固定为十进制:
99
,
130
,
83
,
99
99, 130, 83, 99
99,130,83,99
对应十六进制为 0x63825363。
Client Identifier(客户端标识,Option 61)
值为 0100130220B918,拆解如下:
01 → 硬件类型 = Ethernet(1字节)
00130220B918 → MAC 地址 00:13:02:20:b9:18(6字节)
Windows 系统自动将"硬件类型前缀 + MAC地址"作为客户端ID。
注意:目前有向非 MAC 地址客户端 ID 迁移的趋势(RFC4361),使用 DUID(DHCP 唯一标识符)+ IAID(身份关联标识符)的组合,这样即使更换网卡,客户端 ID 也不变。
Requested IP Address(Option 50)
客户端请求172.16.1.34,这是它在旧网络使用的地址。
Vendor Class Identifier(Option 60)
值为"MSFT 5.0",表示这是 Windows 2000 及以后版本的微软系统。
三、包2详解:DHCPNAK(图6-4)
场景说明
服务器收到客户端请求 172.16.1.34 的消息,检查发现:这个地址不属于当前网络的地址段,无法分配,因此发送 DHCPNAK 拒绝。
数据包关键字段
链路层:
源 MAC: 00:04:5a:9f:9e:80 ← 服务器的 MAC
目标MAC: ff:ff:ff:ff:ff:ff ← 广播
网络层(IP):
源 IP: 10.0.0.1 ← 服务器 IP
目标IP: 255.255.255.255 ← 广播(因为客户端还没有 IP)
传输层(UDP):
源端口: 67 服务器端口
目标端口: 68 客户端端口
DHCP Options:
Option 53: DHCP Message Type = DHCP NAK
Option 54: DHCP Server Identifier = 10.0.0.1
Option 56: Message = "wrong address" ← 错误描述
为什么服务器用广播回复?
因为客户端此时 IP 仍为 0.0.0.0,服务器无法单播回复,只能广播。
客户端依靠
xid
=
0xdb23147d
\text{xid} = \texttt{0xdb23147d}
xid=0xdb23147d 来判断这条 NAK 是发给自己的。
收到 NAK 后客户端的行为
收到 DHCPNAK
|
v
放弃旧地址 172.16.1.34
|
v
重新发送 DHCPDISCOVER(重新开始 DORA 流程)
生成新的 xid = 0x3a681b0b
四、包3详解:DHCPDISCOVER(图6-5)
场景说明
客户端放弃旧地址后,重新广播 DISCOVER,寻找新网络上的 DHCP 服务器。
注意:它仍然在 Requested IP 选项中填写 172.16.1.34(因为它没有其他地址可填),但这只是一个"偏好建议",不是强制要求。
与包1(DHCPREQUEST)的对比
| 字段 | 包1 DHCPREQUEST | 包3 DHCPDISCOVER |
|---|---|---|
| Message Type | DHCP Request | DHCP Discover |
| xid | 0xdb23147d | 0x3a681b0b(新生成) |
| Options数量 | 较少 | 更丰富(8个选项) |
| 目的 | 直接申请旧地址 | 广播寻找服务器 |
新增的关键选项
Option 116(Auto-Configuration)
Option: (t=116,l=1) DHCP Auto-Configuration = AutoConfigure
含义:如果 DHCP 获取失败,客户端是否允许使用链路本地自动配置(APIPA)?
值为 AutoConfigure 表示允许(客户端会在
169.254.0.0
/
16
169.254.0.0/16
169.254.0.0/16 范围内自动选一个地址)。
五、包3选项详解:参数请求列表(图6-6)
DHCPDISCOVER 中的 Option 55(Parameter Request List) 展开后包含 12 个参数请求:
Option (55): Parameter Request List
Length: 12
Value: 010f0306 2c2e2f1f 2179f92b
解析每个字节(每字节对应一个请求的选项编号):
┌────┬────────────────────────────────────────────────┐
│编号│ 含义 │
├────┼────────────────────────────────────────────────┤
│ 1 │ Subnet Mask(子网掩码) │
│ 15 │ Domain Name(域名) │
│ 3 │ Router(默认网关) │
│ 6 │ Domain Name Server(DNS 服务器) │
│ 44 │ NetBIOS over TCP/IP Name Server │
│ 46 │ NetBIOS over TCP/IP Node Type │
│ 47 │ NetBIOS over TCP/IP Scope │
│ 31 │ Perform Router Discover(是否做路由器发现) │
│ 33 │ Static Route(有类静态路由,传统格式) │
│121 │ Classless Static Route(无类静态路由,RFC3442)│
│249 │ Private Classless Static Route(微软私有变体) │
│ 43 │ Vendor-Specific Information(厂商专用信息) │
└────┴────────────────────────────────────────────────┘
为什么有三种静态路由参数?
这是历史遗留问题,三种路由参数对应三个不同的历史阶段:
Option 33:有类路由(Classful)
→
靠地址本身判断网络位
\text{Option 33}:\text{有类路由(Classful)} \to \text{靠地址本身判断网络位}
Option 33:有类路由(Classful)→靠地址本身判断网络位
Option 121:无类路由(Classless)
→
附带子网掩码,RFC3442 定义
\text{Option 121}:\text{无类路由(Classless)} \to \text{附带子网掩码,RFC3442 定义}
Option 121:无类路由(Classless)→附带子网掩码,RFC3442 定义
Option 249:微软私有无类路由
→
与 Option 121 类似,仅 Windows 使用
\text{Option 249}:\text{微软私有无类路由} \to \text{与 Option 121 类似,仅 Windows 使用}
Option 249:微软私有无类路由→与 Option 121 类似,仅 Windows 使用
同时请求三个,是为了兼容不同版本的服务器。
Option 43(厂商专用信息)的用途
在 Windows 系统中,Option 43 配合 Option 60(Vendor Class ID)使用,具体功能包括:
- 控制 NetBIOS 的使用方式
- 指定 DHCP 租约到期时是否自动释放
- 设置默认路由的度量值(metric,即路由优先级)
- 支持微软 NAP(Network Access Protection)网络访问保护
在 Mac OS 系统中,Option 43 用于支持 Apple 的 NetBoot 服务和 BSDP(Boot Server Discovery Protocol)。
六、完整的 ASCII 数据包流向图
时间轴 客户端(Vista) 服务器(10.0.0.1)
─────────────────────────────────────────────────────────────────────────
t=0.000s DHCPREQUEST ──────────────────────────────────────────────>
(xid=0xdb23147d, 请求 172.16.1.34)
检查: 172.16.1.34 不在
本网络地址段,拒绝
t=0.019s DHCPNAK <─────────────────────────────────────────────────
(xid=0xdb23147d, "wrong address")
放弃旧地址,生成新 xid=0x3a681b0b
t=1.083s DHCPDISCOVER ─────────────────────────────────────────────>
(xid=0x3a681b0b, 携带12项参数请求)
查找可用地址,准备提供
t=4.084s DHCPOFFER <──────────────────────────────────────────────
(xid=0x3a681b0b, 提供新地址)
选择该地址,发送 REQUEST 确认
t=4.087s DHCPREQUEST ──────────────────────────────────────────────>
(xid=0x3a681b0b, 广播通知所有服务器)
正式绑定,写入存储
t=4.105s DHCPACK <────────────────────────────────────────────────
(xid=0x3a681b0b, 确认分配)
配置完成,可选做 ARP 冲突检测
─────────────────────────────────────────────────────────────────────────
总耗时约 4.1 秒
七、DNA(检测网络附着)机制说明
本案例中客户端切换网络时,尝试了一种优化策略——直接发 REQUEST,跳过 DISCOVER。
这背后对应 DNA(Detecting Network Attachment,RFC4436/RFC6059) 机制:
DNA 的目标是减少切换网络时重新获取配置的延迟,本例中由于地址前缀不同直接收到 NAK,因此节省了时间不多。
八、完整 C++ 模拟代码(含 NAK 处理和参数请求列表)
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <cstdlib>
#include <ctime>
#include <cstdint>
#include <sstream>
// ===== DHCP 消息类型枚举 =====
enum DHCPMsgType {
DISCOVER = 1,
OFFER = 2,
REQUEST = 3,
DECLINE = 4,
ACK = 5,
NAK = 6,
RELEASE = 7,
INFORM = 8
};
// 消息类型名称(用于打印)
std::string msgTypeName(DHCPMsgType t) {
switch(t) {
case DISCOVER: return "DHCPDISCOVER";
case OFFER: return "DHCPOFFER";
case REQUEST: return "DHCPREQUEST";
case ACK: return "DHCPACK";
case NAK: return "DHCPNAK";
case DECLINE: return "DHCPDECLINE";
default: return "UNKNOWN";
}
}
// ===== 参数请求列表(模拟 Option 55)=====
struct ParameterRequestList {
// 标准网络参数
bool subnetMask = true; // Option 1
bool router = true; // Option 3
bool domainNameServer = true; // Option 6
bool domainName = true; // Option 15
// NetBIOS(微软特有)
bool netbiosNameSrv = true; // Option 44
bool netbiosNodeType = true; // Option 46
// 路由(三种历史版本)
bool staticRoute = true; // Option 33(有类)
bool classlessRoute = true; // Option 121(无类,RFC3442)
bool msClasslessRoute = true; // Option 249(微软私有)
// 其他
bool routerDiscovery = true; // Option 31
bool vendorSpecific = true; // Option 43
void print() const {
std::cout << " [Option 55] 参数请求列表:\n";
if (subnetMask) std::cout << " 1 = 子网掩码(Subnet Mask)\n";
if (router) std::cout << " 3 = 默认网关(Router)\n";
if (domainNameServer) std::cout << " 6 = DNS 服务器\n";
if (domainName) std::cout << " 15 = 域名(Domain Name)\n";
if (netbiosNameSrv) std::cout << " 44 = NetBIOS Name Server\n";
if (netbiosNodeType) std::cout << " 46 = NetBIOS Node Type\n";
if (routerDiscovery) std::cout << " 31 = 路由器发现(Router Discovery)\n";
if (staticRoute) std::cout << " 33 = 有类静态路由(Classful Static Route)\n";
if (classlessRoute) std::cout << " 121= 无类静态路由(Classless,RFC3442)\n";
if (msClasslessRoute) std::cout << " 249= 微软私有无类静态路由\n";
if (vendorSpecific) std::cout << " 43 = 厂商专用信息(Vendor-Specific)\n";
}
};
// ===== DHCP 消息结构 =====
struct DHCPMessage {
DHCPMsgType type;
uint32_t xid = 0; // 事务ID
std::string srcIP = "0.0.0.0";
std::string dstIP = "255.255.255.255";
std::string srcMAC;
std::string ciaddr = "0.0.0.0"; // 客户端当前IP
std::string yiaddr = "0.0.0.0"; // 服务器分配给客户端的IP
std::string siaddr = "0.0.0.0"; // 服务器IP
std::string chaddr; // 客户端硬件地址(MAC)
std::string serverID; // Option 54:服务器标识
std::string requestedIP; // Option 50:请求的IP
std::string hostname; // Option 12:主机名
std::string vendorClass; // Option 60:厂商类标识
std::string clientID; // Option 61:客户端标识
std::string nakMessage; // Option 56:NAK 原因
bool autoConfigureAllowed = false; // Option 116
ParameterRequestList paramReqList; // Option 55
// 打印消息详情
void print() const {
std::cout << "\n--- " << msgTypeName(type) << " ---\n";
std::cout << " xid: 0x" << std::hex << xid << std::dec << "\n";
std::cout << " 源 IP: " << srcIP << ":" << (type==OFFER||type==ACK||type==NAK ? 67 : 68) << "\n";
std::cout << " 目标 IP: " << dstIP << "\n";
std::cout << " chaddr: " << chaddr << "\n";
if (!ciaddr.empty() && ciaddr != "0.0.0.0")
std::cout << " ciaddr: " << ciaddr << "\n";
if (!yiaddr.empty() && yiaddr != "0.0.0.0")
std::cout << " yiaddr: " << yiaddr << " (分配给客户端的 IP)\n";
if (!requestedIP.empty())
std::cout << " [Option 50] 请求的 IP: " << requestedIP << "\n";
if (!hostname.empty())
std::cout << " [Option 12] 主机名: " << hostname << "\n";
if (!vendorClass.empty())
std::cout << " [Option 60] 厂商类ID: " << vendorClass << "\n";
if (!clientID.empty())
std::cout << " [Option 61] 客户端ID: " << clientID << "\n";
if (!serverID.empty())
std::cout << " [Option 54] 服务器ID: " << serverID << "\n";
if (!nakMessage.empty())
std::cout << " [Option 56] NAK原因: \"" << nakMessage << "\"\n";
if (autoConfigureAllowed)
std::cout << " [Option 116] 允许自动配置 (APIPA)\n";
if (type == DISCOVER)
paramReqList.print();
}
};
// ===== DHCP 服务器 =====
class DHCPServer {
public:
std::string ip;
std::string mac;
std::string networkPrefix; // 本网络的地址段前缀,如 "10.0.0."
// 地址池:IP -> 是否已分配
std::map<std::string, bool> pool;
// 预留:xid -> 预留的IP
std::map<uint32_t, std::string> pending;
DHCPServer(const std::string& ip_, const std::string& mac_, const std::string& prefix)
: ip(ip_), mac(mac_), networkPrefix(prefix) {
// 初始化地址池
for (int i = 100; i <= 110; ++i)
pool[networkPrefix + std::to_string(i)] = false;
}
// 检查请求的 IP 是否属于本网络
bool isInNetwork(const std::string& addr) const {
return addr.substr(0, networkPrefix.size()) == networkPrefix;
}
// 处理 REQUEST(客户端跳过 DISCOVER 直接请求旧地址)
DHCPMessage handleDirectRequest(const DHCPMessage& req) {
std::cout << "\n[服务器 " << ip << "] 收到 " << msgTypeName(req.type)
<< ",请求地址: " << req.requestedIP << "\n";
DHCPMessage resp;
resp.xid = req.xid;
resp.srcIP = ip;
resp.srcMAC = mac;
resp.chaddr = req.chaddr;
resp.serverID = ip;
// 检查地址是否属于本网络
if (!isInNetwork(req.requestedIP)) {
// 地址不在本网络,发 NAK
resp.type = NAK;
resp.nakMessage = "wrong address"; // 与 Wireshark 截图一致
std::cout << "[服务器 " << ip << "] 地址 " << req.requestedIP
<< " 不属于本网络前缀 " << networkPrefix << ",发送 NAK\n";
} else if (pool.count(req.requestedIP) && !pool.at(req.requestedIP)) {
// 地址在本网络且空闲,分配
pool[req.requestedIP] = true;
resp.type = ACK;
resp.yiaddr = req.requestedIP;
std::cout << "[服务器 " << ip << "] 分配地址 " << req.requestedIP << ",发送 ACK\n";
} else {
resp.type = NAK;
resp.nakMessage = "address not available";
std::cout << "[服务器 " << ip << "] 地址不可用,发送 NAK\n";
}
return resp;
}
// 处理 DISCOVER
DHCPMessage handleDiscover(const DHCPMessage& disc) {
std::cout << "\n[服务器 " << ip << "] 收到 DHCPDISCOVER,xid=0x"
<< std::hex << disc.xid << std::dec << "\n";
// 找一个空闲地址
std::string offered;
for (auto& [addr, used] : pool) {
if (!used) { offered = addr; break; }
}
DHCPMessage resp;
resp.xid = disc.xid;
resp.srcIP = ip;
resp.srcMAC = mac;
resp.chaddr = disc.chaddr;
resp.serverID = ip;
if (!offered.empty()) {
resp.type = OFFER;
resp.yiaddr = offered;
pending[disc.xid] = offered; // 预留该地址
std::cout << "[服务器 " << ip << "] 提供地址 " << offered << ",发送 OFFER\n";
} else {
std::cout << "[服务器 " << ip << "] 地址池已满,无法提供\n";
}
return resp;
}
// 处理 REQUEST(DORA 流程中的第三步)
DHCPMessage handleRequest(const DHCPMessage& req) {
std::cout << "\n[服务器 " << ip << "] 收到 DHCPREQUEST(DORA),xid=0x"
<< std::hex << req.xid << std::dec << "\n";
DHCPMessage resp;
resp.xid = req.xid;
resp.srcIP = ip;
resp.srcMAC = mac;
resp.chaddr = req.chaddr;
resp.serverID = ip;
if (pending.count(req.xid) && pending[req.xid] == req.yiaddr
&& !pool[req.yiaddr]) {
pool[req.yiaddr] = true; // 正式分配
pending.erase(req.xid);
resp.type = ACK;
resp.yiaddr = req.yiaddr;
std::cout << "[服务器 " << ip << "] 确认分配 " << req.yiaddr << ",发送 ACK\n";
} else {
resp.type = NAK;
resp.nakMessage = "address not available";
std::cout << "[服务器 " << ip << "] 地址不可用,发送 NAK\n";
}
return resp;
}
};
// ===== DHCP 客户端 =====
class DHCPClient {
public:
std::string mac;
std::string hostname;
std::string vendorClass;
std::string assignedIP;
std::string cachedIP; // 记忆的上次使用的 IP
DHCPClient(const std::string& mac_, const std::string& host,
const std::string& vendor, const std::string& lastIP = "")
: mac(mac_), hostname(host), vendorClass(vendor), cachedIP(lastIP) {}
// 生成随机事务 ID
uint32_t newXID() const {
return (uint32_t)rand();
}
// 构造客户端标识(Option 61):硬件类型(01) + MAC
std::string buildClientID() const {
return "01" + mac; // 01 表示 Ethernet
}
// 构造 DHCPREQUEST(DNA 优化,直接请求旧地址)
DHCPMessage buildDirectRequest(uint32_t xid) const {
DHCPMessage msg;
msg.type = REQUEST;
msg.xid = xid;
msg.srcIP = "0.0.0.0";
msg.dstIP = "255.255.255.255";
msg.srcMAC = mac;
msg.chaddr = mac;
msg.requestedIP = cachedIP; // 请求旧地址
msg.hostname = hostname;
msg.vendorClass = vendorClass;
msg.clientID = buildClientID();
return msg;
}
// 构造 DHCPDISCOVER(携带丰富选项)
DHCPMessage buildDiscover(uint32_t xid) const {
DHCPMessage msg;
msg.type = DISCOVER;
msg.xid = xid;
msg.srcIP = "0.0.0.0";
msg.dstIP = "255.255.255.255";
msg.srcMAC = mac;
msg.chaddr = mac;
msg.requestedIP = cachedIP; // 仍然携带偏好地址(不强制)
msg.hostname = hostname;
msg.vendorClass = vendorClass;
msg.clientID = buildClientID();
msg.autoConfigureAllowed = true; // Option 116:允许 APIPA
// Option 55 参数请求列表使用默认值(全部请求)
return msg;
}
// 构造 DHCPREQUEST(选择服务器,DORA 第三步)
DHCPMessage buildRequest(uint32_t xid, const std::string& requestIP,
const std::string& serverID) const {
DHCPMessage msg;
msg.type = REQUEST;
msg.xid = xid;
msg.srcIP = "0.0.0.0";
msg.dstIP = "255.255.255.255";
msg.srcMAC = mac;
msg.chaddr = mac;
msg.yiaddr = requestIP;
msg.serverID = serverID;
msg.hostname = hostname;
return msg;
}
// 完整流程:模拟 Vista 切换网络的场景
void connectToNewNetwork(DHCPServer& server) {
std::cout << "\n========================================\n";
std::cout << "客户端 [" << mac << "] 接入新网络\n";
std::cout << "========================================\n";
// ---- 第一阶段:DNA 优化,直接用旧地址发 REQUEST ----
if (!cachedIP.empty()) {
std::cout << "\n[阶段1] DNA 优化:尝试沿用旧地址 " << cachedIP << "\n";
uint32_t xid1 = newXID();
DHCPMessage req1 = buildDirectRequest(xid1);
req1.print();
DHCPMessage resp1 = server.handleDirectRequest(req1);
resp1.print();
if (resp1.type == ACK) {
assignedIP = resp1.yiaddr;
std::cout << "\n[客户端] 旧地址续用成功,IP=" << assignedIP << "\n";
return;
}
// 收到 NAK,进入第二阶段
std::cout << "\n[客户端] 收到 NAK(" << resp1.nakMessage
<< "),放弃旧地址,重新发起 DISCOVER\n";
}
// ---- 第二阶段:完整 DORA 流程 ----
std::cout << "\n[阶段2] 完整 DORA 流程\n";
uint32_t xid2 = newXID();
std::cout << "新事务ID: 0x" << std::hex << xid2 << std::dec << "\n";
// Step 1: DISCOVER
std::cout << "\n--- Step 1: DISCOVER ---\n";
DHCPMessage disc = buildDiscover(xid2);
disc.print();
// Step 2: OFFER
std::cout << "\n--- Step 2: OFFER ---\n";
DHCPMessage offer = server.handleDiscover(disc);
offer.print();
if (offer.type != OFFER || offer.yiaddr == "0.0.0.0") {
std::cout << "[客户端] 没有收到有效 OFFER,流程终止\n";
return;
}
std::cout << "\n[客户端] 收到 OFFER,决定请求 IP=" << offer.yiaddr << "\n";
// Step 3: REQUEST
std::cout << "\n--- Step 3: REQUEST ---\n";
DHCPMessage req2 = buildRequest(xid2, offer.yiaddr, offer.serverID);
req2.print();
// Step 4: ACK
std::cout << "\n--- Step 4: ACK ---\n";
DHCPMessage ack = server.handleRequest(req2);
ack.print();
if (ack.type == ACK) {
assignedIP = ack.yiaddr;
std::cout << "\n[客户端] 配置成功!IP=" << assignedIP << "\n";
} else {
std::cout << "\n[客户端] 获取地址失败\n";
}
std::cout << "\n========================================\n";
}
};
// ===== 主函数 =====
int main() {
srand((unsigned)time(nullptr));
// 创建 DHCP 服务器:IP=10.0.0.1,网络前缀 "10.0.0."
DHCPServer server("10.0.0.1", "00:04:5a:9f:9e:80", "10.0.0.");
// 创建客户端(模拟 Vista 笔记本):
// MAC = 00:13:02:20:b9:18
// 上次使用的 IP = 172.16.1.34(属于旧网络,新网络无效)
DHCPClient client("00:13:02:20:b9:18", "vista", "MSFT 5.0", "172.16.1.34");
// 执行切换网络流程
client.connectToNewNetwork(server);
return 0;
}
https://godbolt.org/z/fsz8oshE1
编译与运行:
g++ -std=c++17 -o dhcp_vista dhcp_vista.cpp && ./dhcp_vista
九、关键概念总结
| 概念 | 说明 |
|---|---|
| xid(事务ID) | 32位随机数,用于匹配同一次交互的请求与响应 |
| chaddr | 客户端 MAC 地址,填在 BOOTP 头部 |
| ciaddr | 客户端"当前"IP,没有则为 0.0.0.0 0.0.0.0 0.0.0.0 |
| yiaddr | 服务器分配给客户端的 IP(“your IP address”) |
| DHCPNAK | 服务器拒绝客户端请求,客户端必须重新 DISCOVER |
| Option 50 | 客户端希望获取的 IP(偏好,非强制) |
| Option 54 | 服务器标识符,多服务器场景下区分哪台服务器响应 |
| Option 55 | 参数请求列表,客户端告知服务器希望获取哪些配置 |
| Option 60 | 厂商类标识,如 “MSFT 5.0” 表示微软 Windows |
| Option 61 | 客户端标识,Windows 默认使用 01+MAC 地址 |
| Option 116 | 是否允许 APIPA 自动配置( 169.254.0.0 / 16 169.254.0.0/16 169.254.0.0/16) |
| DUID | DHCP 唯一标识符,RFC4361 提出,不依赖 MAC 地址 |
| IAID | 身份关联标识符,与 DUID 配合用于 DHCPv6 |
DHCP 动态主机配置协议 — 详细中文解析
1. 什么是 DHCP?
DHCP(Dynamic Host Configuration Protocol,动态主机配置协议) 是一种网络协议,让设备(客户端)自动从服务器获取 IP 地址及其他网络配置信息,而不需要手动设置。
通俗理解:就像你去酒店前台(DHCP 服务器)登记入住,前台给你分配一个房间号(IP 地址),告诉你 WiFi 密码、早餐时间等附加信息,并约定你住几天(租约时长)。退房后房间归还,下次入住可能给你不同房间。
2. DHCP 报文交换流程(DORA)
DHCP 的完整流程由四种报文构成,简称 DORA:
Discover → Offer → Request → ACK
下面结合 Wireshark 抓包截图,逐步分析每一个报文。
3. 完整交互时序图
4. 逐报文详细分析
4.1 报文 [1][2] — 旧地址请求与拒绝(NAK)
客户端开机后,先尝试使用之前记住的 IP 地址 172.16.1.34,向服务器发送 DHCP Request。
服务器判断 172.16/12 前缀不在当前网络中,回复 DHCP NAK(否定确认),告知客户端该地址无效。
客户端收到 NAK 后,放弃旧地址,重新发起完整的 DORA 流程。
4.2 报文 [3] — DHCP Discover(发现)
客户端从 0.0.0.0 广播到 255.255.255.255,请求网络中的 DHCP 服务器响应。
关键字段:
| 字段 | 值 | 说明 |
|---|---|---|
| 源地址 | 0.0.0.0 | 客户端还没有IP |
| 目标地址 | 255.255.255.255 | 广播,所有人都能收到 |
| Transaction ID | 0x3a681b0b | 本次会话唯一标识 |
| 客户端MAC | 00:13:02:20:b9:18 | 用于服务器识别客户端 |
4.3 报文 [4] — DHCP Offer(提供)图6-7
服务器收到 Discover 后,回复一个 DHCP Offer,提供可用的 IP 地址和配置参数。
BOOTP 层关键字段:
| 字段 | 值 | 说明 |
|---|---|---|
| 消息类型 | Boot Reply (2) | 服务器回复 |
| 客户端IP | 0.0.0.0 | 客户端还没有IP |
| 你的(客户端)IP | 10.0.0.57 | 服务器给客户端分配的地址 |
| 下一个服务器IP | 10.0.0.1 | DHCP服务器自身地址 |
| 客户端MAC | 00:13:02:20:b9:18 | 确认目标客户端 |
DHCP Options(选项)层:
| 选项编号 | 含义 | 值 |
|---|---|---|
| t=53 | 报文类型 | DHCP Offer |
| t=54 | 服务器标识 | 10.0.0.1 |
| t=51 | IP地址租约时长 | 12小时 |
| t=58 | T1 续约时间 | 6小时 |
| t=59 | T2 重绑定时间 | 10小时30分 |
| t=1 | 子网掩码 | 255.255.255.128 |
| t=28 | 广播地址 | 10.0.0.127 |
| t=3 | 默认网关(路由器) | 10.0.0.1 |
| t=6 | DNS服务器 | 10.0.0.1 |
| t=15 | 域名 | “home” |
租约时间轴:
0h 6h(T1) 10.5h(T2) 12h(过期)
|-----------|-----------|-----------|
正常使用 尝试续约 重新绑定 地址释放
- T1(6小时):客户端尝试向原 DHCP 服务器续约(单播)
- T2(10.5小时):若 T1 续约失败,向任意服务器广播续约(重绑定)
- 过期(12小时):若仍未续约成功,放弃地址,重新发起 DORA
子网分析:
子网掩码255.255.255.128=/25,表示该子网: - 网络地址: 10.0.0.0 10.0.0.0 10.0.0.0
- 广播地址: 10.0.0.127 10.0.0.127 10.0.0.127
- 可用主机范围: 10.0.0.1 10.0.0.1 10.0.0.1 ~ 10.0.0.126 10.0.0.126 10.0.0.126
- 可用主机数量:
2
7
−
2
=
126
2^7 - 2 = 126
27−2=126 台
子网掩码的二进制表示:
255.255.255.128 = 11111111.11111111.11111111 ⏟ 网络位 .1 0000000 ⏟ 主机位 255.255.255.128 = \underbrace{11111111.11111111.11111111}_{网络位}.1\underbrace{0000000}_{主机位} 255.255.255.128=网络位 11111111.11111111.11111111.1主机位 0000000
4.4 报文 [5] — DHCP Request(请求确认)图6-8
客户端收到 Offer 后,决定接受地址 10.0.0.57,向服务器发送 DHCP Request 正式请求。
为什么还要用广播?
虽然客户端已选定服务器,但仍使用广播发送,目的是通知网络中其他所有 DHCP 服务器:"我已经选了 10.0.0.1 这台服务器,你们不用给我分配地址了。"其他服务器看到 Server Identifier 不是自己,就会撤回之前的提议。
关键字段对比(与 Discover 的区别):
| 字段 | Discover [3] | Request [5] |
|---|---|---|
| 报文类型 | DHCP Discover | DHCP Request |
| 请求IP (t=50) | (没有) | 10.0.0.57 |
| 服务器标识 (t=54) | (没有) | 10.0.0.1 |
| 主机名 (t=12) | (没有) | “vista” |
| 厂商类别 (t=60) | (可能有) | “MSFT 5.0”(Windows Vista) |
4.5 报文 [6] — DHCP ACK(确认)图6-9
服务器确认绑定,回复 DHCP ACK,正式将 10.0.0.57 分配给该客户端。
与 DHCP Offer 相比,ACK 多了一个字段:
| 选项 | 值 | 说明 |
|---|---|---|
| t=81 | vista.home | 客户端完全限定域名(FQDN) |
收到 ACK 后,客户端即可正式使用 10.0.0.57,配置完成。
5. 报文流程总结(ASCII 示意图)
客户端 服务器 (10.0.0.1)
| |
|--- [广播] DHCP Request ------------>| 尝试续用旧地址
|<-- [广播] DHCP NAK ----------------| 旧地址无效,拒绝
| |
|--- [广播] DHCP Discover ----------->| 我需要一个IP!
|<-- [广播] DHCP Offer --------------| 给你 10.0.0.57,租12小时
| |
|--- [广播] DHCP Request ------------>| 我接受 10.0.0.57
|<-- [广播] DHCP ACK ----------------| 确认!地址已分配
| |
| 现在使用 IP: 10.0.0.57 |
| 子网掩码: 255.255.255.128 |
| 网关/DNS: 10.0.0.1 |
6. 本例网络拓扑
本例中
10.0.0.1这台机器身兼三职:DHCP 服务器 + DNS 服务器 + 默认路由器,这在家庭网络中很常见(通常就是你家的路由器)。
7. Windows 相关命令
# 释放 DHCP 租约(断开IP)
C:\> ipconfig /release
# 重新获取 DHCP 配置
C:\> ipconfig /renew
# 查看当前所有网络配置详情
C:\> ipconfig /all
ipconfig /all 输出示例(对应本例):
Connection-specific DNS Suffix . : home
Physical Address. . . . . . . . . : 00-13-02-20-B9-18
DHCP Enabled. . . . . . . . . . . : Yes
IPv4 Address. . . . . . . . . . . : 10.0.0.57 (Preferred)
Subnet Mask . . . . . . . . . . . : 255.255.255.128
Lease Obtained. . . . . . . . . . : 2008-12-21 23:31:48
Lease Expires . . . . . . . . . . : 2008-12-22 11:31:40
Default Gateway . . . . . . . . . : 10.0.0.1
DHCP Server . . . . . . . . . . . : 10.0.0.1
DNS Servers . . . . . . . . . . . : 10.0.0.1
8. Linux 相关命令
# 释放 DHCP 租约
Linux# dhclient -r
# 重新获取 DHCP 配置
Linux# dhclient
9. 关键概念速查
| 概念 | 解释 |
|---|---|
| Transaction ID | 每次 DORA 会话的唯一标识,客户端生成,用于匹配请求和响应 |
| 广播地址 255.255.255.255 | 发给局域网所有设备,DHCP 发现阶段必须用广播 |
| BOOTP | DHCP 的前身协议,DHCP 报文在其基础上扩展了 Options 字段 |
| 租约 (Lease) | IP 地址的使用期限,到期需续约或重新申请 |
| T1 | 租约的 50%(6小时)时尝试向原服务器续约 |
| T2 | 租约的 87.5%(10.5小时)时向任意服务器广播续约 |
| FQDN | 完全限定域名,例如 vista.home,= 主机名 + 域名 |
| NAK | 否定确认,服务器拒绝客户端的地址请求 |
| ACD | 地址冲突检测,客户端获得地址后用 ARP 探测是否有冲突 |
DHCP 客户端状态机 — 详细中文解析
1. 什么是状态机?
状态机(State Machine) 是一种描述系统行为的模型:系统在任意时刻处于某个状态,当收到某个事件(消息、超时等)后,按照规则转换到另一个状态。
通俗比喻:就像红绿灯,当前只能是红/黄/绿三种状态之一,收到定时器信号后切换到下一个状态。DHCP 客户端也类似,每个状态规定了"我现在能干什么、等什么消息"。
DHCP 协议在客户端和服务器上都运行状态机,下面专门分析客户端状态机。
2. 状态机全图(Mermaid)
3. 各状态详细说明
3.1 INIT — 初始状态
进入条件: 客户端刚启动,或租约彻底失效,或收到 NACK
在此状态做什么:
- 向网络广播 DHCPDISCOVER 报文
- 源地址用
0.0.0.0(还没有IP),目标地址用255.255.255.255(广播)
离开条件: 发送 DISCOVER 后,立即进入 Selecting 状态
INIT
|
| 发送 DHCPDISCOVER(广播到 255.255.255.255)
v
Selecting
3.2 Selecting — 选择状态
在此状态做什么:
- 等待并收集网络中所有 DHCP 服务器的 DHCPOFFER 报文
- 可能收到多个 Offer(来自不同服务器),需要从中选一个
选择策略(通常): 选最先到达的那个 Offer
离开条件: 做出选择后,发送 DHCPREQUEST,进入 Requesting 状态
Selecting
| (可能多次自循环,收集多个 Offer)
|
| 选定服务器,发送 DHCPREQUEST(仍然广播)
v
Requesting
注意:此阶段的 DHCPREQUEST 依然用广播,目的是通知未被选中的服务器撤回它们的 Offer,释放预留的地址资源。
3.3 Requesting — 请求状态
在此状态做什么:
- 等待所选服务器的最终确认
可能发生的事:
| 收到的消息 | 下一步操作 | 进入状态 |
|-----------|-----------|---------|
| DHCPACK(且地址无冲突) | 接受地址,启动 T1/T2 计时器 | Bound |
| DHCPACK(但地址冲突,ACD检测失败) | 发送 DHCPDECLINE,拒绝地址 | INIT |
| DHCPNACK | 地址被拒绝 | INIT |
3.4 Bound — 绑定状态(正常使用)
进入条件: 收到服务器 ACK,接受地址
在此状态做什么:
- 客户端正常使用分配的 IP 地址
- 同时启动两个计时器:
T 1 = 0.5 × 租约时长 T_1 = 0.5 \times \text{租约时长} T1=0.5×租约时长
T 2 = 0.875 × 租约时长 T_2 = 0.875 \times \text{租约时长} T2=0.875×租约时长
以本例(租约12小时)为例:
T 1 = 0.5 × 12 = 6 小时 T_1 = 0.5 \times 12 = 6 \text{ 小时} T1=0.5×12=6 小时
T 2 = 0.875 × 12 = 10.5 小时 T_2 = 0.875 \times 12 = 10.5 \text{ 小时} T2=0.875×12=10.5 小时
租约时间轴:
0h 6h(T1) 10.5h(T2) 12h(租约到期)
|---------|----------|-----------|
Bound Renewing Rebinding INIT(断网)
正常使用 向原服务器 向任意服务器
单播续约 广播续约
离开条件: T1 到期,进入 Renewing 状态
3.5 Renewing — 续约状态
进入条件: T1(
0.5
×
0.5 \times
0.5× 租约时长)到期
在此状态做什么:
- 向原 DHCP 服务器发送 DHCPREQUEST(此时用单播,不用广播)
- 等待服务器回应
| 收到的消息 | 下一步 | 进入状态 |
|---|---|---|
| DHCPACK | 续约成功,重置 T1/T2 | Bound |
| 超时到 T2 仍无回应 | 原服务器可能宕机,扩大范围 | Rebinding |
用单播而不是广播,是因为客户端已知服务器地址,没必要打扰整个网络。
3.6 Rebinding — 重绑定状态
进入条件: T2(
0.875
×
0.875 \times
0.875× 租约时长)到期,即续约失败
在此状态做什么:
- 向任意 DHCP 服务器广播 DHCPREQUEST(原服务器可能已宕机)
- 相当于"找任何一个服务器帮我续期"
| 收到的消息 | 下一步 | 进入状态 |
|---|---|---|
| DHCPACK | 重绑定成功,可能获得新地址 | Bound |
| DHCPNACK 或租约到期 | 彻底失去地址,断网 | INIT |
4. 完整状态转换表
| 当前状态 | 触发事件 | 动作 | 下一状态 |
|---|---|---|---|
| INIT | 启动 | 发送 DHCPDISCOVER(广播) | Selecting |
| Selecting | 收到 DHCPOFFER | 收集 Offer,继续等待 | Selecting |
| Selecting | 决定选择服务器 | 发送 DHCPREQUEST(广播) | Requesting |
| Requesting | 收到 ACK(接受) | 配置IP,启动T1/T2 | Bound |
| Requesting | 收到 ACK(地址冲突) | 发送 DHCPDECLINE | INIT |
| Requesting | 收到 NACK | 放弃,重新开始 | INIT |
| Bound | T1 到期 | 向原服务器发送 REQUEST(单播) | Renewing |
| Renewing | 收到 ACK | 续约成功,重置计时器 | Bound |
| Renewing | T2 到期 | 向任意服务器广播 REQUEST | Rebinding |
| Rebinding | 收到 ACK | 重绑定成功 | Bound |
| Rebinding | 收到 NACK 或租约到期 | 放弃地址,断网 | INIT |
5. 正常流程 vs 异常流程(ASCII)
正常流程(加粗路径):
INIT
|-- 发送 DISCOVER
Selecting
|-- 发送 REQUEST
Requesting
|-- 收到 ACK
Bound <--------+
|T1到期 |
Renewing | 收到 ACK(续约成功)
+--------------+
续约失败流程:
Bound
|-- T1到期
Renewing(单播续约,无响应)
|-- T2到期
Rebinding(广播续约)
|-- 租约到期 or NACK
INIT(重新申请,断网风险)
6. 关键概念补充
DHCPDECLINE 是什么?
客户端收到 ACK、地址看起来合法,但在使用前用 ARP(地址解析协议) 探测该地址是否已被占用(这叫 ACD,地址冲突检测)。
如果发现冲突:
- 发送 DHCPDECLINE 告诉服务器"这个地址有人用,我不要了"
- 返回 INIT 重新申请
为什么 T1 用单播,T2 用广播?
- T1 阶段:客户端认为原服务器还活着,直接单播联系它,效率高,不打扰网络
- T2 阶段:原服务器长时间未响应,可能已宕机,只能广播求助其他服务器
租约到期后会怎样?
客户端必须立即停止使用该 IP 地址。如果没有其他网络连接,设备将断网,直到重新完成 DORA 流程获取新地址。
DHCPv6 与 IPv6 地址生命周期 — 详细中文解析
1. DHCPv6 概述
DHCPv6 是 IPv6 网络中的地址配置协议,与 DHCPv4 目标相似,但设计上有重要区别。
DHCPv6 的两种工作模式
DHCPv6
|
+--- 有状态模式(Stateful)
| 类似 DHCPv4,服务器负责分配 IPv6 地址 + 其他配置
|
+--- 无状态模式(Stateless)
客户端自己生成 IPv6 地址(SLAAC)
仅从 DHCPv6 获取额外配置(如 DNS 服务器地址)
通俗理解:
- 有状态:服务器记录"谁用了哪个地址"(有账本)
- 无状态:客户端自己配置地址,只找服务器要点补充信息(无账本)
DNS 服务器地址的另一种获取方式
除了 DHCPv6,还可以通过 ICMPv6 路由器通告(Router Advertisement) 消息获取 DNS 服务器地址(RFC 6106),这在纯无状态环境中很常用。
2. IPv6 地址的特殊性
与 IPv4 不同,IPv6 主机通常一个网卡同时拥有多个 IPv6 地址,每个地址都有独立的生命周期计时器,决定:
- 这个地址还能用多久
- 这个地址能用来做什么
每个 IPv6 地址有两个关键时间参数:
| 参数 | 含义 |
|---|---|
| 首选生命周期(Preferred Lifetime) | 地址处于"首选"状态的时长,期间可无限制使用 |
| 有效生命周期(Valid Lifetime) | 地址仍然有效的总时长,期间旧连接可继续,之后完全失效 |
关系如下:
首选生命周期
≤
有效生命周期
\text{首选生命周期} \leq \text{有效生命周期}
首选生命周期≤有效生命周期
3. IPv6 地址状态机(完整图)
4. 各状态详细说明
4.1 Tentative — 暂定状态(传统 DAD)
进入条件: 地址刚被选定,尚未验证唯一性
在此状态能做什么:
- 只能用于 IPv6 邻居发现协议(NDP) 中的 DAD 过程
- 不能作为任何普通数据包的源地址或目标地址
DAD(重复地址检测)是什么?
类比:你搬进新小区,先在门口贴张纸条"我要用 3 号车位",等几天看有没有人来反对。如果没人反对,就确认这个车位归你。
DAD 的具体流程:
- 发送一个 NS(Neighbor Solicitation,邻居请求) 报文,目标是自己想用的地址
- 等待一段时间,看网络上是否有其他设备回应
- 若无回应:地址唯一,DAD 成功,进入 Preferred 状态
- 若有回应:地址冲突,DAD 失败,丢弃该地址
4.2 Optimistic — 乐观状态(乐观 DAD,RFC 4429)
进入条件: 地址刚被选定,采用乐观 DAD 策略
核心思想: 不等 DAD 完成就先用地址(有限制地用),减少等待时间,加速连接建立。
在此状态能做什么:
- 按照 Deprecated(弃用) 地址的规则使用
- 即:可以维持现有连接,但不应用于发起新连接
- DAD 仍在后台进行
注意: 乐观状态不是一个完全独立的状态,它是 DAD 进行期间的特殊使用规则。一个地址可以同时处于乐观状态和弃用状态(当首选生命周期在 DAD 完成前就已到期时)。
4.3 Preferred — 首选状态(正常使用)
进入条件: DAD 成功,地址确认唯一
在此状态能做什么:
- 可以作为源地址或目标地址,无任何限制
- 正常发起新连接,接收数据
离开条件: 首选生命周期(Preferred Lifetime)到期,进入 Deprecated 状态
4.4 Deprecated — 弃用状态
进入条件: 首选生命周期超时
在此状态能做什么:
- 可以继续维持已有的 TCP 等传输层连接(不能强行中断已有会话)
- 不应该用于发起新连接(系统会优先选择 Preferred 地址建立新连接)
通俗比喻:你的公交卡余额不足,还能坐完这一趟车(维持现有连接),但不建议再刷这张卡上新的车(不发起新连接)。
离开条件: 有效生命周期(Valid Lifetime)到期,进入 Invalid 状态
4.5 Invalid — 无效状态
进入条件: 有效生命周期超时
在此状态能做什么:
- 什么都不能做,地址彻底失效
- 任何使用该地址的连接都会中断
- 地址可以被重新分配给其他设备
5. 地址生命周期时间轴
地址选定
|
|<--- DAD 检测期 --->|
| |
Tentative Preferred
(仅NDP) (无限制使用)
|
|<---- 首选生命周期 ---->|
| |
开始计时 Preferred Timeout
|
Deprecated
(仅维持旧连接)
|
|<-- 弃用期 -->|
| |
继续倒计时 Valid Timeout
|
Invalid
(彻底失效)
用公式表示各阶段时长关系:
t
首选
=
Preferred Lifetime(首选生命周期)
t_{\text{首选}} = \text{Preferred Lifetime(首选生命周期)}
t首选=Preferred Lifetime(首选生命周期)
t
有效
=
Valid Lifetime(有效生命周期)
t_{\text{有效}} = \text{Valid Lifetime(有效生命周期)}
t有效=Valid Lifetime(有效生命周期)
t
弃用期
=
t
有效
−
t
首选
t_{\text{弃用期}} = t_{\text{有效}} - t_{\text{首选}}
t弃用期=t有效−t首选
t
首选
≤
t
有效
t_{\text{首选}} \leq t_{\text{有效}}
t首选≤t有效
6. 有状态 vs 无状态模式对比
| 对比项 | 有状态 DHCPv6 | 无状态 DHCPv6 + SLAAC |
|---|---|---|
| 地址由谁分配 | DHCPv6 服务器 | 客户端自己生成(SLAAC) |
| 服务器是否记录地址分配 | 是(有账本) | 否(无账本) |
| 能否获取 DNS 等配置 | 是 | 是(仅获取附加配置) |
| 适用场景 | 企业网络,需要精确管理 | 家庭/简单网络 |
7. 关键概念速查
| 概念 | 解释 |
|---|---|
| DAD | 重复地址检测,验证地址在局域网内唯一 |
| 乐观 DAD | DAD 未完成时就有限使用地址,减少等待延迟 |
| 首选生命周期 | 地址处于 Preferred 状态的时长 |
| 有效生命周期 | 地址仍然有效(含 Deprecated)的总时长 |
| SLAAC | 无状态地址自动配置,客户端根据路由器通告自己生成地址 |
| NDP | 邻居发现协议,IPv6 中代替 ARP 的协议,也承载 DAD |
| RA | 路由器通告(Router Advertisement),可携带 DNS 信息 |
DHCPv6 协议详解
6.2.5.2 DHCPv6 消息格式
基本概念
DHCPv6(Dynamic Host Configuration Protocol for IPv6)是为 IPv6 网络设计的地址自动分配协议。
- 传输方式:封装在 UDP/IPv6 数据报中
- 客户端端口:546
- 服务器端口:547
- 源地址范围:链路本地(link-scoped)地址
消息格式概览
DHCPv6 有两种消息格式:
客户端 <-----> 服务器:使用"客户端/服务器消息格式"
客户端 <---> 中继代理 <-----> 服务器:使用"中继代理消息格式"
格式一:客户端/服务器消息格式
0 15 16 31
+----------------+------------------+
| Message Type | Transaction ID |
| (8 bits) | (24 bits) |
+----------------+------------------+
| |
| Options (variable) |
| |
+-----------------------------------+
字段说明:
- Message Type(8位):消息类型,如 SOLICIT、ADVERTISE、REQUEST 等
- Transaction ID(24位):事务ID,用于匹配请求与响应
- Options(可变长度):携带地址、租约时间、服务器标识等关键信息
格式二:中继代理消息格式
0 15 16 31
+----------------+------------------+
| Message Type | Hop Count |
| (8 bits) | (8 bits) |
+----------------+------------------+
| |
| Link Address |
| (128 bits / 16 bytes) |
| |
+-----------------------------------+
| |
| Peer Address |
| (128 bits / 16 bytes) |
| |
+-----------------------------------+
| |
| Options (variable) |
| |
+-----------------------------------+
字段说明:
- Message Type:中继专用类型,如 RELAY-FORW(中继转发)、RELAY-REPL(中继回复)
- Hop Count:跳数,防止中继无限循环转发
- Link Address:服务器用来识别客户端所在链路的全局 IPv6 地址
- Peer Address:发送待转发消息的上一跳中继或客户端的地址
- Options:始终包含 Relay Message Option(即被转发的完整消息),也可含其他选项
中继转发流程示意
Client
|
| (SOLICIT, 使用链路本地地址)
v
Relay Agent 1 <-- Peer Address 记录 Client 地址
| Link Address 记录本链路全局地址
| (RELAY-FORW)
v
Relay Agent 2 <-- 中继可以链式嵌套
|
| (RELAY-FORW)
v
DHCPv6 Server
|
| (RELAY-REPL)
v
Relay Agent 2
|
v
Relay Agent 1
|
v
Client (REPLY)
DHCPv6 与 DHCPv4 的关键区别
| 特性 | DHCPv4 | DHCPv6 |
|---|---|---|
| 发现方式 | 广播(255.255.255.255) | 组播(ff02::1:2,所有中继/服务器) |
| 源地址范围 | 0.0.0.0(无地址时) | 链路本地地址 |
| 消息格式 | 基于 BOOTP | 全新格式,无 BOOTP 兼容 |
| 关键信息携带 | 固定字段+Options | 几乎全部用 Options |
DHCPv6 客户端发送请求时,目标地址为组播地址
ff02::1:2(所有 DHCP 中继代理和服务器),源地址为链路本地作用域地址。
DHCPv6 消息类型对照表
| DHCPv6 消息 | 值 | 对应 DHCPv4 消息 | 作用说明 |
|---|---|---|---|
| SOLICIT | 1 | DISCOVER | 客户端寻找服务器 |
| ADVERTISE | 2 | OFFER | 服务器响应,告知可提供服务 |
| REQUEST | 3 | REQUEST | 客户端正式请求地址/配置 |
| CONFIRM | 4 | REQUEST | 确认已有地址仍然有效 |
| RENEW | 5 | REQUEST | 更新租约 |
| REBIND | 6 | DISCOVER | 重新绑定(联系任意服务器) |
| REPLY | 7 | ACK/NAK | 服务器回应请求 |
| RELEASE | 8 | RELEASE | 客户端释放地址 |
| DECLINE | 9 | DECLINE | 客户端拒绝分配的地址 |
| RECONFIGURE | 10 | FORCERENEW | 服务器主动要求客户端重新获取配置 |
| INFORMATION-REQUEST | 11 | INFORM | 只请求配置信息,不请求地址 |
| RELAY-FORW | 12 | N/A | 中继代理转发消息到服务器 |
| RELAY-REPL | 13 | N/A | 服务器回复中继代理 |
| LEASEQUERY | 14 | LEASEQUERY | 查询租约信息 |
| LEASEQUERY-REPLY | 15 | LEASE{UNASSIGNED/UNKNOWN/ACTIVE} | 回复租约查询 |
| LEASEQUERY-DONE | 16 | LEASEQUERYDONE | 批量租约查询结束 |
| LEASEQUERY-DATA | 17 | N/A | 批量租约查询数据 |
典型四步握手流程(无中继)
6.2.5.3 身份关联(Identity Association,IA)
什么是 IA?
IA 是 DHCPv6 客户端与服务器之间用于标识一组地址集合的逻辑标识符。
核心要素:
- 每个 IA 有一个 IAID(IA 标识符),由客户端自主选择并通知服务器
- 每个 IA 只能关联一个网络接口
- 每个请求 DHCPv6 地址的接口至少需要一个 IA
IA 中包含的配置信息: - 一个或多个 IPv6 地址
- 每个地址的首选生存期(Preferred Lifetime)和有效生存期(Valid Lifetime)
- 租约计时器 T 1 T_1 T1、 T 2 T_2 T2 及总租约时长
地址生命周期
每个地址有两个生存期参数(来自 [RFC4862]):
Preferred Lifetime
≥
address is preferred for use
\text{Preferred Lifetime} \geq \text{address is preferred for use}
Preferred Lifetime≥address is preferred for use
Valid Lifetime
≥
address is still valid (deprecated after preferred expires)
\text{Valid Lifetime} \geq \text{address is still valid (deprecated after preferred expires)}
Valid Lifetime≥address is still valid (deprecated after preferred expires)
地址状态转换:
分配 --> [Preferred状态] --> (Preferred过期) --> [Deprecated状态] --> (Valid过期) --> [失效]
|<------- Preferred Lifetime ------->|
|<---------------------- Valid Lifetime ------------------>|
非临时地址 vs 临时地址
| 特性 | 非临时地址(IA_NA) | 临时地址(IA_TA) |
|---|---|---|
| 选项类型 | OPTION_IA_NA | OPTION_IA_TA |
| 含 T1/T2 | 是 | 否 |
| 地址来源 | 服务器策略分配 | 含随机数,保护隐私 |
| 主要用途 | 常规通信 | 防追踪,隐私保护 |
| 再生频率 | 按 T1/T2 更新 | 更频繁(使用新随机数) |
临时地址没有 T 1 T_1 T1、 T 2 T_2 T2 的原因:其生存期通常依据同时获取的非临时地址的 T 1 T_1 T1、 T 2 T_2 T2 来决定,详见 [RFC4941]。
IA 选项格式
IA for Nontemporary Address(IA_NA)
0 15 16 31
+----------------+------------------+
| OPTION_IA_NA | Option Length |
+----------------+------------------+
| |
| IAID (4 bytes) |
| |
+-----------------------------------+
| T1 |
+-----------------------------------+
| T2 |
+-----------------------------------+
| |
| IA_NA Options (variable) |
| (含具体 IPv6 地址及其租约信息) |
| |
+-----------------------------------+
IA for Temporary Address(IA_TA)
0 15 16 31
+----------------+------------------+
| OPTION_IA_TA | Option Length |
+----------------+------------------+
| |
| IAID (4 bytes) |
| |
+-----------------------------------+
| |
| IA_TA Options (variable) |
| (含具体临时 IPv6 地址及租约信息) |
| |
+-----------------------------------+
字段说明:
- OPTION_IA_NA / OPTION_IA_TA:选项类型编号
- Option Length:后续内容的字节长度
- IAID(4字节):该 IA 的唯一标识,客户端自行选定
- T1:客户端应向原服务器发起 RENEW 续约的时间点(秒)
- T2:若 T1 时未收到响应,客户端向任意服务器发起 REBIND 的时间点(秒)
- IA_NA/IA_TA Options:嵌套选项,描述具体分配的 IPv6 地址和对应租约
T1/T2 与租约更新流程
设租约总时长为
T
l
e
a
s
e
T_{lease}
Tlease,通常:
T
1
≈
0.5
×
T
l
e
a
s
e
T_1 \approx 0.5 \times T_{lease}
T1≈0.5×Tlease
T
2
≈
0.8
×
T
l
e
a
s
e
T_2 \approx 0.8 \times T_{lease}
T2≈0.8×Tlease
时间轴:
0 T1 T2 Valid Lifetime
|----------|----------|----------|
| | |
| | +-- 地址失效
| |
| +-- 发送 REBIND(向任意服务器)
|
+-- 发送 RENEW(向原服务器)
服务器地址分配策略
服务器根据以下信息决定给 IA 分配哪些地址:
- 请求到达的链路(Link)
- 客户端的 DUID(DHCP Unique Identifier,见下节 6.2.5.4)
- 客户端在 DHCP Options 中提供的其他信息
总结
DHCPv6 DUID 详解
6.2.5.4 DHCP 唯一标识符(DUID)
什么是 DUID?
DUID(DHCP Unique Identifier)是 DHCPv6 中用于唯一标识一台客户端或服务器的标识符。
它的两个核心设计目标:
- 全局唯一:在整个网络中不重复
- 持久稳定:不因网络接口变更、重启等情况而改变
DUID 的用途
服务器 --> 用 DUID 识别客户端 --> 决定分配哪些地址(IA)和配置信息
客户端 --> 用 DUID 识别目标服务器 --> 只与特定服务器交互
DUID 对客户端和服务器来说是不透明的(opaque),也就是说双方不解析其内部结构,只做整体比较(类似"黑盒标签")。
DUID 的三种类型
[RFC3315] 定义了三种 DUID 类型(未来也可能扩展新类型,由 IANA 维护):
| 类型 | 全称 | 构成要素 | 适用场景 |
|---|---|---|---|
| DUID-LLT | Link-Layer + Time | 硬件类型 + 时间戳 + 链路层地址 | 推荐使用,有稳定存储的设备 |
| DUID-EN | Enterprise Number | 企业编号(PEN)+ 厂商自定义 | 企业/厂商设备,无链路层地址时 |
| DUID-LL | Link-Layer only | 硬件类型 + 链路层地址 | 无稳定存储但链路层地址固定的设备 |
各类型格式详解
DUID-LLT(推荐类型)
0 15 16 31
+----------------+------------------+
| DUID Type=1 | Hardware Type |
| (16 bits) | (16 bits) |
+----------------+------------------+
| |
| Time (32 bits) |
| |
+-----------------------------------+
| |
| Link-Layer Address (variable) |
| |
+-----------------------------------+
字段说明:
- DUID Type:固定为
1,表示 DUID-LLT - Hardware Type(16位):硬件类型,来自 [RFC0826](ARP 协议中定义),如以太网为
1 - Time(32位):时间戳,单位为秒,起始时间为 UTC 2000年1月1日 00:00:00
T = 当前UTC时间(秒) − 2000-01-01 00:00:00 UTC(秒) m o d 2 32 T = \text{当前UTC时间(秒)} - \text{2000-01-01 00:00:00 UTC(秒)} \mod 2^{32} T=当前UTC时间(秒)−2000-01-01 00:00:00 UTC(秒)mod232该时间戳会在 2 32 2^{32} 232 秒后归零,即约 136 年后(2136年)发生回绕。
- Link-Layer Address(可变长度):可选取主机任意一个网络接口的 MAC 地址
重要特性: - 一旦选定,所有接口上的流量都使用同一个 DUID,不随接口切换而改变
- 即使生成该 DUID 所用的网络接口被移除,DUID 仍需保持不变
- 因此需要稳定的持久化存储(如写入磁盘/Flash)来保存 DUID
DUID-EN(企业编号类型)
0 15 16 31
+----------------+------------------+
| DUID Type=2 | |
| (16 bits) | |
+----------------+ +
| Enterprise Number (PEN, 32 bits) |
+-----------------------------------+
| |
| Vendor-assigned ID (variable) |
| |
+-----------------------------------+
字段说明:
- DUID Type:固定为
2 - Enterprise Number(32位):即 PEN(Private Enterprise Number),由 IANA 分配给企业的唯一编号
- Vendor-assigned ID:厂商自定义的唯一标识内容
什么是 PEN(私有企业编号)?
PEN 是 IANA 颁发给企业的 32 位唯一编号,常用于 SNMP 网络管理协议。截至 2011 年中期,已分配约 38,000 个。
DUID-LL(仅链路层地址)
0 15 16 31
+----------------+------------------+
| DUID Type=3 | Hardware Type |
| (16 bits) | (16 bits) |
+----------------+------------------+
| |
| Link-Layer Address (variable) |
| |
+-----------------------------------+
字段说明:
- DUID Type:固定为
3 - Hardware Type(16位):与 DUID-LLT 相同
- Link-Layer Address(可变长度):设备链路层地址(MAC 地址)
使用限制: - 适用于没有稳定存储但链路层地址固定的设备(如某些嵌入式设备)
- 禁止在链路层地址来自可拔插接口的设备上使用(因为拔掉后地址就变了,破坏唯一性)
三种 DUID 对比总结
DUID-LLT: [Type=1][HW Type][ Time戳 ][ MAC地址 ]
DUID-EN: [Type=2][ PEN企业编号 ][ 厂商自定义 ]
DUID-LL: [Type=3][HW Type][ MAC地址 ]
| 对比项 | DUID-LLT | DUID-EN | DUID-LL |
|---|---|---|---|
| 是否推荐 | 是(首选) | 企业场景 | 备选 |
| 需要稳定存储 | 是 | 是 | 否 |
| 含时间戳 | 是 | 否 | 否 |
| 含链路层地址 | 是 | 否 | 是 |
| 接口可移除 | 可以(需持久化) | 不涉及 | 不可以 |
| 时间回绕年份 | 2136年 | 无 | 无 |
选型决策流程
C++ 示例:生成 DUID-LLT
#include <iostream>
#include <cstdint>
#include <cstring>
#include <ctime>
#include <iomanip>
#include <vector>
// DUID-LLT 结构体
// 格式:[Type 2B][HW Type 2B][Time 4B][Link-Layer Address 可变]
struct DUID_LLT {
uint16_t duid_type; // 固定为 1(DUID-LLT)
uint16_t hw_type; // 硬件类型,以太网为 1(来自 RFC0826)
uint32_t time_stamp; // 从 2000-01-01 00:00:00 UTC 起的秒数 mod 2^32
uint8_t mac[6]; // 链路层地址(MAC 地址),这里固定6字节示例
};
// 计算从 2000-01-01 00:00:00 UTC 到当前时间的秒数(mod 2^32)
uint32_t get_duid_timestamp() {
// 2000-01-01 00:00:00 UTC 对应的 Unix 时间戳(秒)
// Unix 时间从 1970-01-01 开始,相差 30 年
const time_t epoch_2000 = 946684800; // 2000-01-01 00:00:00 UTC
time_t now = time(nullptr); // 当前 Unix 时间戳
// 计算差值,转为 uint32_t 自动完成 mod 2^32
uint32_t delta = static_cast<uint32_t>(now - epoch_2000);
return delta;
}
// 将字节序列打印为十六进制
void print_hex(const uint8_t* data, size_t len) {
for (size_t i = 0; i < len; i++) {
if (i > 0) std::cout << ":";
std::cout << std::hex << std::setw(2) << std::setfill('0')
<< static_cast<int>(data[i]);
}
std::cout << std::dec; // 恢复十进制输出
}
// 将 DUID-LLT 序列化为字节数组(网络字节序,大端)
std::vector<uint8_t> serialize_duid_llt(const DUID_LLT& duid) {
std::vector<uint8_t> buf;
// DUID Type(大端,高字节在前)
buf.push_back((duid.duid_type >> 8) & 0xFF);
buf.push_back(duid.duid_type & 0xFF);
// Hardware Type(大端)
buf.push_back((duid.hw_type >> 8) & 0xFF);
buf.push_back(duid.hw_type & 0xFF);
// Time(大端,32位)
buf.push_back((duid.time_stamp >> 24) & 0xFF);
buf.push_back((duid.time_stamp >> 16) & 0xFF);
buf.push_back((duid.time_stamp >> 8) & 0xFF);
buf.push_back(duid.time_stamp & 0xFF);
// Link-Layer Address(MAC 地址,6字节)
for (int i = 0; i < 6; i++) {
buf.push_back(duid.mac[i]);
}
return buf;
}
int main() {
// 模拟一个以太网 MAC 地址
// 实际使用时应从系统网络接口获取
uint8_t mac_addr[6] = {0x00, 0x1A, 0x2B, 0x3C, 0x4D, 0x5E};
// 构建 DUID-LLT
DUID_LLT duid;
duid.duid_type = 1; // DUID-LLT 类型编号
duid.hw_type = 1; // 以太网(Ethernet)= 1
duid.time_stamp = get_duid_timestamp(); // 计算时间戳
memcpy(duid.mac, mac_addr, 6); // 复制 MAC 地址
// 打印各字段
std::cout << "=== DUID-LLT 生成结果 ===" << std::endl;
std::cout << "DUID Type : " << duid.duid_type
<< " (1 = DUID-LLT)" << std::endl;
std::cout << "HW Type : " << duid.hw_type
<< " (1 = Ethernet)" << std::endl;
std::cout << "Time Stamp : " << duid.time_stamp
<< " 秒(自2000-01-01 UTC起)" << std::endl;
std::cout << "MAC Address: ";
print_hex(duid.mac, 6);
std::cout << std::endl;
// 序列化并打印完整字节序列
std::vector<uint8_t> raw = serialize_duid_llt(duid);
std::cout << "\n完整 DUID-LLT 字节序列(共 "
<< raw.size() << " 字节):" << std::endl;
print_hex(raw.data(), raw.size());
std::cout << std::endl;
// 说明时间回绕年份
// 2^32 秒 ≈ 136 年,从 2000 年起约到 2136 年
std::cout << "\n注意:时间戳将在约 2136 年发生 2^32 回绕" << std::endl;
return 0;
}
https://godbolt.org/z/h6Mr91Mqr
编译与运行:
g++ -std=c++11 -o duid_llt duid_llt.cpp
./duid_llt
示例输出:
=== DUID-LLT 生成结果 ===
DUID Type : 1 (1 = DUID-LLT)
HW Type : 1 (1 = Ethernet)
Time Stamp : 798364217 秒(自2000-01-01 UTC起)
MAC Address: 00:1a:2b:3c:4d:5e
完整 DUID-LLT 字节序列(共 14 字节):
00:01:00:01:2f:a1:xx:xx:00:1a:2b:3c:4d:5e
注意:时间戳将在约 2136 年发生 2^32 回绕
时间戳回绕说明
DUID-LLT 中的时间戳为 32 位无符号整数,其最大值为:
T
m
a
x
=
2
32
−
1
=
4,294,967,295
秒
≈
136
年
T_{max} = 2^{32} - 1 = 4{,}294{,}967{,}295 \text{ 秒} \approx 136 \text{ 年}
Tmax=232−1=4,294,967,295 秒≈136 年
从起始点 2000年1月1日 计算:
2000
+
136
=
2136
年发生回绕(归零)
2000 + 136 = 2136 \text{ 年发生回绕(归零)}
2000+136=2136 年发生回绕(归零)
回绕后时间戳重新从 0 开始增长,但由于 DUID 的 MAC 地址部分不同,仍能保证全局唯一性(两台在不同时期使用相同 MAC 地址的设备极不可能同时存在于网络中)。
总结
DUID 是 DHCPv6 的"设备身份证"
三种类型:
DUID-LLT --> 最常用,需持久存储,含时间戳防碰撞
DUID-EN --> 企业设备专用,靠 PEN + 厂商ID保证唯一
DUID-LL --> 轻量备选,无持久存储设备,接口必须固定
关键原则:
1. 同一设备在所有接口上只使用一个 DUID
2. DUID 一旦生成就不应改变(即使接口变化)
3. DUID-LLT 和 DUID-LL 都依赖 RFC0826 定义的硬件类型编号
DHCPv6 协议操作详解
6.2.5.5 协议操作(Protocol Operation)
一、客户端如何决定是否使用 DHCPv6?
客户端不是直接启动 DHCPv6 的,而是先通过 ICMPv6 路由器通告(Router Advertisement,RA) 中的标志位来决定。
路由器通告中有两个关键标志位:
| 标志位 | 名称 | 含义 |
|---|---|---|
| M | Managed Address Configuration | 置1表示:IPv6 地址可通过 DHCPv6 获取 |
| O | Other Configuration | 置1表示:除地址外的其他配置(DNS等)可通过 DHCPv6 获取 |
M、O 标志位组合含义
M=0, O=0
⇒
不使用 DHCPv6,纯 SLAAC 自动配置地址
\text{M=0, O=0} \Rightarrow \text{不使用 DHCPv6,纯 SLAAC 自动配置地址}
M=0, O=0⇒不使用 DHCPv6,纯 SLAAC 自动配置地址
M=0, O=1
⇒
无状态 DHCPv6(Stateless DHCPv6),地址用 SLAAC,其他配置用 DHCPv6
\text{M=0, O=1} \Rightarrow \text{无状态 DHCPv6(Stateless DHCPv6),地址用 SLAAC,其他配置用 DHCPv6}
M=0, O=1⇒无状态 DHCPv6(Stateless DHCPv6),地址用 SLAAC,其他配置用 DHCPv6
M=1, O=0
⇒
有状态 DHCPv6 分配地址,但不获取其他配置(最少用)
\text{M=1, O=0} \Rightarrow \text{有状态 DHCPv6 分配地址,但不获取其他配置(最少用)}
M=1, O=0⇒有状态 DHCPv6 分配地址,但不获取其他配置(最少用)
M=1, O=1
⇒
完整有状态 DHCPv6,地址和其他配置都由 DHCPv6 提供
\text{M=1, O=1} \Rightarrow \text{完整有状态 DHCPv6,地址和其他配置都由 DHCPv6 提供}
M=1, O=1⇒完整有状态 DHCPv6,地址和其他配置都由 DHCPv6 提供
M=0, O=0 --> 纯 SLAAC(无 DHCPv6)
M=0, O=1 --> 无状态 DHCPv6(SLAAC 地址 + DHCPv6 配置)
M=1, O=0 --> 有状态 DHCPv6(仅地址,最少用)
M=1, O=1 --> 完整有状态 DHCPv6(地址 + 配置)
二、完整启动流程
客户端上线后的完整步骤:
三、四消息交换流程(标准流程)
这是 DHCPv6 最完整的标准流程,共四步:
Client DHCPv6 Server
| |
|------- SOLICIT ---------------------->| (1) 寻找服务器
| (null/mcast; xid; DUID, opts) |
| | 服务器确定配置方案
|<------ ADVERTISE --------------------| (2) 服务器响应报价
| (mcast; xid, IP, serverID, opts)|
| |
| 客户端收集所有响应,选择最优服务器 |
| |
|------- REQUEST ---------------------->| (3) 正式请求地址
| (mcast; address, serverID, xid) |
| | 服务器提交绑定
|<------ REPLY ------------------------| (4) 确认分配
| (mcast; xid, options) |
| |
| 客户端完成配置 |
| |
|======= 地址冲突检测(推荐)============|
| |
|------- Check for Conflict (DAD) ----->| DAD 重复地址检测
| |
|------- DECLINE (若有冲突) ----------->| (可选) 拒绝该地址
各步骤详解
步骤 1:SOLICIT(寻找服务器)
- 客户端向组播地址
ff02::1:2发送 SOLICIT - 携带:事务ID(xid)、客户端 DUID、请求的选项
- 源地址可以是全零(null)或链路本地地址
步骤 2:ADVERTISE(服务器响应) - 一个或多个服务器回复 ADVERTISE
- 携带:可分配的 IP 地址、服务器 ID(DUID)、配置选项
- 客户端收集所有响应,选择最优服务器(类似招标选标)
步骤 3:REQUEST(正式请求) - 客户端向选定服务器发送 REQUEST
- 携带:目标服务器 ID、期望地址、事务 ID
- 仍使用组播发送(让未被选中的服务器也知道结果)
步骤 4:REPLY(确认分配) - 服务器提交绑定(Binding),返回 REPLY
- 携带:最终分配的地址和所有配置选项
- 客户端完成配置
四、两消息交换流程(快速流程)
在以下情况下,可跳过 SOLICIT/ADVERTISE,直接使用两步:
- 服务器位置已知(如之前记录过)
- 使用无状态 DHCPv6(只要配置,不要地址)
- 使用了 Rapid Commit 选项(见 6.2.9 节)
Client DHCPv6 Server
| |
|------- REQUEST ---------------------->| (1) 直接请求
| (mcast; address, serverID, xid) |
| |
|<------ REPLY ------------------------| (2) 直接确认
| (mcast; xid, options) |
| |
| 客户端完成配置 |
五、路由器交互阶段(RA 阶段)
在正式进入 DHCPv6 流程之前,客户端先与路由器交互:
Client Router
| |
|--- ROUTER SOLICIT ------>| ICMPv6 RS:我上线了,有路由器吗?
| |
|<-- ROUTER ADVERTISE -----| ICMPv6 RA:有!M=1, O=1,请用 DHCPv6
| |
| 读取 M、O 标志位 |
| 决定是否启动 DHCPv6 |
注意:Router Solicit / Router Advertise 是 ICMPv6 的消息,不是 DHCPv6 的消息。它们属于 NDP(邻居发现协议),在第 8 章详细介绍。
六、绑定(Binding)的构成
服务器在 REPLY 时会"提交绑定",绑定由以下三要素唯一确定:
Binding
=
DUID
+
IA类型
+
IAID
\text{Binding} = \text{DUID} + \text{IA类型} + \text{IAID}
Binding=DUID+IA类型+IAID
| 组成部分 | 含义 |
|---|---|
| DUID | 客户端唯一标识符(见 6.2.5.4) |
| IA 类型 | 非临时地址(IA_NA)/ 临时地址(IA_TA)/ 前缀(IA_PD) |
| IAID | 客户端自选的 32 位接口标识,区分同一设备的不同接口 |
- 一个绑定可以包含一个或多个租约(lease)
- 一次 DHCPv6 事务可以同时操作多个绑定
一次事务可处理:
Binding_1 (DUID + IA_NA + IAID_1) --> lease_A, lease_B
Binding_2 (DUID + IA_TA + IAID_2) --> lease_C
七、地址冲突检测(DAD)
收到地址后,客户端推荐执行 DAD(Duplicate Address Detection,重复地址检测):
客户端收到地址 2001:db8::1
|
+--> 发送 ICMPv6 Neighbor Solicitation(探测该地址是否有人用)
目标:solicited-node 组播地址
|
+--> 等待一段时间
|
+-- 无响应 --> 地址可用,正式使用
|
+-- 有响应 --> 地址冲突!
发送 DHCPv6 DECLINE 通知服务器
服务器标记该地址为冲突,重新分配
八、完整流程总览(含 RA 阶段)
九、C++ 示例:模拟 DHCPv6 消息结构与绑定管理
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <cstdint>
#include <sstream>
#include <iomanip>
// IA 类型枚举
enum class IAType {
IA_NA, // 非临时地址
IA_TA, // 临时地址
IA_PD // 前缀委派
};
// 将 IAType 转为字符串
std::string ia_type_str(IAType t) {
switch (t) {
case IAType::IA_NA: return "IA_NA(非临时地址)";
case IAType::IA_TA: return "IA_TA(临时地址)";
case IAType::IA_PD: return "IA_PD(前缀委派)";
}
return "Unknown";
}
// 租约(Lease):一个绑定中的单条地址记录
struct Lease {
std::string ipv6_address; // 分配的 IPv6 地址
uint32_t preferred_life; // 首选生存期(秒)
uint32_t valid_life; // 有效生存期(秒)
};
// 绑定(Binding):服务器维护的核心数据结构
// 唯一键 = DUID + IA类型 + IAID
struct Binding {
std::string duid; // 客户端 DUID(唯一标识)
IAType ia_type; // IA 类型
uint32_t iaid; // 接口标识(客户端自选)
std::vector<Lease> leases; // 该绑定下的租约列表
// 生成绑定的唯一键字符串,便于在 map 中存储
std::string key() const {
std::ostringstream oss;
oss << duid << "_" << static_cast<int>(ia_type)
<< "_" << std::hex << iaid;
return oss.str();
}
};
// DHCPv6 消息类型
enum class MsgType {
SOLICIT = 1,
ADVERTISE = 2,
REQUEST = 3,
REPLY = 7,
DECLINE = 9
};
std::string msg_type_str(MsgType t) {
switch (t) {
case MsgType::SOLICIT: return "SOLICIT";
case MsgType::ADVERTISE: return "ADVERTISE";
case MsgType::REQUEST: return "REQUEST";
case MsgType::REPLY: return "REPLY";
case MsgType::DECLINE: return "DECLINE";
}
return "UNKNOWN";
}
// 简化的 DHCPv6 消息
struct DHCPv6Message {
MsgType msg_type;
uint32_t xid; // 事务 ID(24位有效,这里用32位存储)
std::string client_duid; // 客户端 DUID
std::string server_duid; // 服务器 DUID(REQUEST/REPLY 时使用)
uint32_t iaid; // 接口标识
IAType ia_type;
std::string requested_addr; // 请求/分配的地址
};
// 简化的 DHCPv6 服务器
class DHCPv6Server {
public:
std::string server_duid;
// 地址池(简化:字符串列表)
std::vector<std::string> address_pool;
// 绑定表:key -> Binding
std::map<std::string, Binding> binding_table;
// 已用地址集合
std::vector<std::string> used_addresses;
DHCPv6Server(const std::string& duid) : server_duid(duid) {
// 初始化地址池
address_pool = {
"2001:db8::1",
"2001:db8::2",
"2001:db8::3"
};
}
// 从地址池分配一个未使用地址
std::string allocate_address() {
for (const auto& addr : address_pool) {
bool in_use = false;
for (const auto& u : used_addresses) {
if (u == addr) { in_use = true; break; }
}
if (!in_use) {
used_addresses.push_back(addr);
return addr;
}
}
return ""; // 地址池耗尽
}
// 处理 SOLICIT,返回 ADVERTISE
DHCPv6Message handle_solicit(const DHCPv6Message& solicit) {
std::cout << "[服务器] 收到 SOLICIT,客户端 DUID: "
<< solicit.client_duid << std::endl;
std::string addr = allocate_address();
if (addr.empty()) {
std::cout << "[服务器] 地址池已满!" << std::endl;
}
DHCPv6Message adv;
adv.msg_type = MsgType::ADVERTISE;
adv.xid = solicit.xid; // 事务ID保持一致
adv.server_duid = server_duid;
adv.client_duid = solicit.client_duid;
adv.requested_addr = addr;
adv.ia_type = solicit.ia_type;
adv.iaid = solicit.iaid;
std::cout << "[服务器] 发送 ADVERTISE,提议地址: "
<< addr << std::endl;
return adv;
}
// 处理 REQUEST,提交绑定,返回 REPLY
DHCPv6Message handle_request(const DHCPv6Message& req) {
std::cout << "[服务器] 收到 REQUEST,请求地址: "
<< req.requested_addr << std::endl;
// 构建绑定
Binding b;
b.duid = req.client_duid;
b.ia_type = req.ia_type;
b.iaid = req.iaid;
Lease lease;
lease.ipv6_address = req.requested_addr;
lease.preferred_life = 3600; // 1小时首选生存期
lease.valid_life = 7200; // 2小时有效生存期
b.leases.push_back(lease);
// 存入绑定表(DUID + IA类型 + IAID 作为唯一键)
binding_table[b.key()] = b;
std::cout << "[服务器] 提交绑定: " << b.key() << std::endl;
DHCPv6Message reply;
reply.msg_type = MsgType::REPLY;
reply.xid = req.xid;
reply.server_duid = server_duid;
reply.client_duid = req.client_duid;
reply.requested_addr = req.requested_addr;
reply.ia_type = req.ia_type;
reply.iaid = req.iaid;
return reply;
}
// 处理 DECLINE(地址冲突时客户端发送)
void handle_decline(const DHCPv6Message& dec) {
std::cout << "[服务器] 收到 DECLINE,地址冲突: "
<< dec.requested_addr << std::endl;
// 从已用列表中标记该地址为冲突(实际应标记为不可用)
for (auto it = used_addresses.begin();
it != used_addresses.end(); ++it) {
if (*it == dec.requested_addr) {
used_addresses.erase(it);
break;
}
}
std::cout << "[服务器] 已将地址 " << dec.requested_addr
<< " 标记为冲突,不再分配" << std::endl;
}
// 打印当前绑定表
void print_bindings() const {
std::cout << "\n=== 服务器绑定表 ===" << std::endl;
if (binding_table.empty()) {
std::cout << "(空)" << std::endl;
return;
}
for (const auto& kv : binding_table) {
const Binding& b = kv.second;
std::cout << "绑定键: " << kv.first << std::endl;
std::cout << " DUID : " << b.duid << std::endl;
std::cout << " IA类型 : " << ia_type_str(b.ia_type) << std::endl;
std::cout << " IAID : 0x" << std::hex << b.iaid
<< std::dec << std::endl;
for (const auto& lease : b.leases) {
std::cout << " 租约地址: " << lease.ipv6_address << std::endl;
std::cout << " 首选生存期: " << lease.preferred_life
<< " 秒" << std::endl;
std::cout << " 有效生存期: " << lease.valid_life
<< " 秒" << std::endl;
}
}
}
};
// 模拟四消息交换流程
void simulate_four_message_exchange() {
std::cout << "========== 四消息交换流程演示 ==========" << std::endl;
DHCPv6Server server("DUID-SERVER-001");
// 客户端信息
std::string client_duid = "DUID-CLIENT-ABC";
uint32_t xid = 0x123456; // 事务ID(24位)
uint32_t iaid = 0x00000001; // 接口标识
// --- 步骤 1:客户端发送 SOLICIT ---
DHCPv6Message solicit;
solicit.msg_type = MsgType::SOLICIT;
solicit.xid = xid;
solicit.client_duid = client_duid;
solicit.ia_type = IAType::IA_NA;
solicit.iaid = iaid;
std::cout << "\n[客户端] 发送 " << msg_type_str(solicit.msg_type)
<< " (xid=0x" << std::hex << xid << std::dec << ")" << std::endl;
// --- 步骤 2:服务器回复 ADVERTISE ---
DHCPv6Message advertise = server.handle_solicit(solicit);
// --- 步骤 3:客户端发送 REQUEST ---
DHCPv6Message request;
request.msg_type = MsgType::REQUEST;
request.xid = xid; // 同一事务ID
request.client_duid = client_duid;
request.server_duid = advertise.server_duid;
request.requested_addr = advertise.requested_addr;
request.ia_type = IAType::IA_NA;
request.iaid = iaid;
std::cout << "\n[客户端] 收到 ADVERTISE,选择服务器: "
<< advertise.server_duid << std::endl;
std::cout << "[客户端] 发送 " << msg_type_str(request.msg_type)
<< ",请求地址: " << request.requested_addr << std::endl;
// --- 步骤 4:服务器提交绑定,回复 REPLY ---
DHCPv6Message reply = server.handle_request(request);
std::cout << "\n[客户端] 收到 " << msg_type_str(reply.msg_type)
<< ",已配置地址: " << reply.requested_addr << std::endl;
std::cout << "[客户端] 执行 DAD 冲突检测(推荐步骤)..." << std::endl;
std::cout << "[客户端] DAD 通过,地址可用!" << std::endl;
// 打印服务器绑定表
server.print_bindings();
}
int main() {
simulate_four_message_exchange();
return 0;
}
https://godbolt.org/z/843caqPo4
编译与运行:
g++ -std=c++11 -o dhcpv6_sim dhcpv6_sim.cpp
./dhcpv6_sim
示例输出:
========== 四消息交换流程演示 ==========
[客户端] 发送 SOLICIT (xid=0x123456)
[服务器] 收到 SOLICIT,客户端 DUID: DUID-CLIENT-ABC
[服务器] 发送 ADVERTISE,提议地址: 2001:db8::1
[客户端] 收到 ADVERTISE,选择服务器: DUID-SERVER-001
[客户端] 发送 REQUEST,请求地址: 2001:db8::1
[服务器] 收到 REQUEST,请求地址: 2001:db8::1
[服务器] 提交绑定: DUID-CLIENT-ABC_0_1
[客户端] 收到 REPLY,已配置地址: 2001:db8::1
[客户端] 执行 DAD 冲突检测(推荐步骤)...
[客户端] DAD 通过,地址可用!
=== 服务器绑定表 ===
绑定键: DUID-CLIENT-ABC_0_1
DUID : DUID-CLIENT-ABC
IA类型 : IA_NA(非临时地址)
IAID : 0x1
租约地址: 2001:db8::1
首选生存期: 3600 秒
有效生存期: 7200 秒
十、关键概念对比:四消息 vs 两消息
四消息交换(标准):
SOLICIT --> ADVERTISE --> REQUEST --> REPLY
适用:首次获取地址,服务器位置未知
两消息交换(快速):
REQUEST --> REPLY
适用:
(1) 服务器位置已知(如续约)
(2) 无状态 DHCPv6(只要 DNS 等配置)
(3) 使用了 Rapid Commit 选项
十一、总结
ICMPv6 RA 中的 M/O 标志位决定是否使用 DHCPv6
|
+-- M=1 --> 有状态 DHCPv6 --> 分配 IPv6 地址
+-- O=1 --> 无状态 DHCPv6 --> 只获取 DNS/NTP 等配置
+-- 均为0 --> 纯 SLAAC
DHCPv6 核心交换流程:
标准四步:SOLICIT -> ADVERTISE -> REQUEST -> REPLY
快速两步:REQUEST -> REPLY(已知服务器或Rapid Commit)
服务器绑定 = DUID + IA类型 + IAID
一个绑定可含多个租约
一次事务可操作多个绑定
配置完成后推荐执行 DAD 冲突检测
冲突则发送 DECLINE,服务器重新分配
更多推荐
所有评论(0)