一、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 开始:

DHCP 服务器 客户端 DHCP 服务器 客户端 alt [续租成功] [续租失败] DHCPREQUEST(我想续租当前 IP) DHCPACK(续租成功) DHCPNAK(拒绝,需重新获取)

五、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”)
serverIDOption 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 时序图:
DHCP服务器 10.0.0.1 客户端(Vista) 0.0.0.0 DHCP服务器 10.0.0.1 客户端(Vista) 0.0.0.0 第一阶段:尝试沿用旧地址(失败) 第二阶段:重新发起完整 DORA 流程 [包1] DHCPREQUEST 请求旧地址 172.16.1.34 xid=0xdb23147d [包2] DHCPNAK "wrong address" xid=0xdb23147d [包3] DHCPDISCOVER 新xid=0x3a681b0b [包4] DHCPOFFER xid=0x3a681b0b [包5] DHCPREQUEST xid=0x3a681b0b [包6] DHCPACK xid=0x3a681b0b

二、包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 TypeDHCP RequestDHCP Discover
xid0xdb23147d0x3a681b0b(新生成)
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) 机制:

ACK

NAK 或超时

主机切换网络

是否记得上次的 IP?

发送单播 ARP 或 DHCPREQUEST
尝试沿用旧地址

直接发 DHCPDISCOVER

服务器响应?

成功,减少延迟

完整 DORA 流程

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
DUIDDHCP 唯一标识符,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. 完整交互时序图

DHCP服务器 (10.0.0.1) 客户端 (0.0.0.0) DHCP服务器 (10.0.0.1) 客户端 (0.0.0.0) 第一轮:旧地址被拒绝 第二轮:DORA 完整流程 [1] DHCP Request (Transaction ID: 0xdb23147d) 请求旧地址 172.16.1.34 [2] DHCP NAK (Transaction ID: 0xdb23147d) 拒绝:该地址不在本网段 [3] DHCP Discover (Transaction ID: 0x3a681b0b) 广播:我需要一个IP地址 [4] DHCP Offer (Transaction ID: 0x3a681b0b) 提供:10.0.0.57,租期12小时 [5] DHCP Request (Transaction ID: 0x3a681b0b) 确认:我接受 10.0.0.57 [6] DHCP ACK (Transaction ID: 0x3a681b0b) 确认:地址已分配,生效

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 ID0x3a681b0b本次会话唯一标识
客户端MAC00:13:02:20:b9:18用于服务器识别客户端

4.3 报文 [4] — DHCP Offer(提供)图6-7

服务器收到 Discover 后,回复一个 DHCP Offer,提供可用的 IP 地址和配置参数。
BOOTP 层关键字段:

字段说明
消息类型Boot Reply (2)服务器回复
客户端IP0.0.0.0客户端还没有IP
你的(客户端)IP10.0.0.57服务器给客户端分配的地址
下一个服务器IP10.0.0.1DHCP服务器自身地址
客户端MAC00:13:02:20:b9:18确认目标客户端

DHCP Options(选项)层:

选项编号含义
t=53报文类型DHCP Offer
t=54服务器标识10.0.0.1
t=51IP地址租约时长12小时
t=58T1 续约时间6小时
t=59T2 重绑定时间10小时30分
t=1子网掩码255.255.255.128
t=28广播地址10.0.0.127
t=3默认网关(路由器)10.0.0.1
t=6DNS服务器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 272=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 DiscoverDHCP 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=81vista.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. 本例网络拓扑

默认网关

客户端
MAC: 00:13:02:20:b9:18
IP: 10.0.0.57
主机名: vista

10.0.0.1
同时担任三角色

DHCP服务器
分配IP地址

DNS服务器
解析域名

路由器
转发数据包

本例中 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 发现阶段必须用广播
BOOTPDHCP 的前身协议,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)

启动/无地址信息

发送 DHCPDISCOVER
(广播)

收集 DHCPOFFER
(等待多个服务器响应)

选定服务器
发送 DHCPREQUEST

收到 NACK
或租约到期

收到 ACK 但地址冲突
发送 DHCPDECLINE

收到 ACK(接受)
获得 T1、T2 计时器

T1 到期
(租约50%时)

收到 ACK(续约成功)

T2 到期
(续约失败)

收到 ACK(重绑定成功)

收到 NACK
或租约彻底到期

INIT

Selecting

Requesting

Bound

Renewing

Rebinding

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/T2Bound
超时到 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/T2Bound
Requesting收到 ACK(地址冲突)发送 DHCPDECLINEINIT
Requesting收到 NACK放弃,重新开始INIT
BoundT1 到期向原服务器发送 REQUEST(单播)Renewing
Renewing收到 ACK续约成功,重置计时器Bound
RenewingT2 到期向任意服务器广播 REQUESTRebinding
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 地址状态机(完整图)

地址选择完成
(传统 DAD 路径)

地址选择完成
(乐观 DAD 路径)

DAD 成功
(地址唯一,可正常使用)

DAD 失败
(地址冲突,丢弃)

DAD 成功

首选生命周期到期
(DAD 期间就过期)

DAD 失败

首选生命周期超时
(Preferred Timeout)

有效生命周期超时
(Valid Timeout)

地址彻底失效

Tentative

Optimistic

Preferred

Deprecated

Invalid

4. 各状态详细说明

4.1 Tentative — 暂定状态(传统 DAD)

进入条件: 地址刚被选定,尚未验证唯一性
在此状态能做什么:

  • 只能用于 IPv6 邻居发现协议(NDP) 中的 DAD 过程
  • 不能作为任何普通数据包的源地址或目标地址
    DAD(重复地址检测)是什么?

类比:你搬进新小区,先在门口贴张纸条"我要用 3 号车位",等几天看有没有人来反对。如果没人反对,就确认这个车位归你。
DAD 的具体流程:

  1. 发送一个 NS(Neighbor Solicitation,邻居请求) 报文,目标是自己想用的地址
  2. 等待一段时间,看网络上是否有其他设备回应
  3. 若无回应:地址唯一,DAD 成功,进入 Preferred 状态
  4. 若有回应:地址冲突,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重复地址检测,验证地址在局域网内唯一
乐观 DADDAD 未完成时就有限使用地址,减少等待延迟
首选生命周期地址处于 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 的关键区别


特性DHCPv4DHCPv6
发现方式广播(255.255.255.255)组播(ff02::1:2,所有中继/服务器)
源地址范围0.0.0.0(无地址时)链路本地地址
消息格式基于 BOOTP全新格式,无 BOOTP 兼容
关键信息携带固定字段+Options几乎全部用 Options

DHCPv6 客户端发送请求时,目标地址为组播地址 ff02::1:2(所有 DHCP 中继代理和服务器),源地址为链路本地作用域地址。

DHCPv6 消息类型对照表


DHCPv6 消息对应 DHCPv4 消息作用说明
SOLICIT1DISCOVER客户端寻找服务器
ADVERTISE2OFFER服务器响应,告知可提供服务
REQUEST3REQUEST客户端正式请求地址/配置
CONFIRM4REQUEST确认已有地址仍然有效
RENEW5REQUEST更新租约
REBIND6DISCOVER重新绑定(联系任意服务器)
REPLY7ACK/NAK服务器回应请求
RELEASE8RELEASE客户端释放地址
DECLINE9DECLINE客户端拒绝分配的地址
RECONFIGURE10FORCERENEW服务器主动要求客户端重新获取配置
INFORMATION-REQUEST11INFORM只请求配置信息,不请求地址
RELAY-FORW12N/A中继代理转发消息到服务器
RELAY-REPL13N/A服务器回复中继代理
LEASEQUERY14LEASEQUERY查询租约信息
LEASEQUERY-REPLY15LEASE{UNASSIGNED/UNKNOWN/ACTIVE}回复租约查询
LEASEQUERY-DONE16LEASEQUERYDONE批量租约查询结束
LEASEQUERY-DATA17N/A批量租约查询数据

典型四步握手流程(无中继)

DHCPv6 Server Client DHCPv6 Server Client SOLICIT (寻找服务器,目标 ff02::1:2) ADVERTISE (告知可分配的地址和配置) REQUEST (正式请求指定地址) REPLY (确认分配,含地址和租约信息)

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 Lifetimeaddress 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 Lifetimeaddress is still valid (deprecated after preferred expires)
地址状态转换:

分配 --> [Preferred状态] --> (Preferred过期) --> [Deprecated状态] --> (Valid过期) --> [失效]
          |<------- Preferred Lifetime ------->|
          |<---------------------- Valid Lifetime ------------------>|

非临时地址 vs 临时地址


特性非临时地址(IA_NA)临时地址(IA_TA)
选项类型OPTION_IA_NAOPTION_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} T10.5×Tlease
T 2 ≈ 0.8 × T l e a s e T_2 \approx 0.8 \times T_{lease} T20.8×Tlease

时间轴:
0          T1         T2        Valid Lifetime
|----------|----------|----------|
     |           |         |
     |           |         +-- 地址失效
     |           |
     |           +-- 发送 REBIND(向任意服务器)
     |
     +-- 发送 RENEW(向原服务器)

服务器地址分配策略

服务器根据以下信息决定给 IA 分配哪些地址:

  1. 请求到达的链路(Link)
  2. 客户端的 DUID(DHCP Unique Identifier,见下节 6.2.5.4)
  3. 客户端在 DHCP Options 中提供的其他信息

总结

DHCPv6 消息

是否经过中继?

客户端/服务器格式
Message Type + Transaction ID + Options

中继代理格式
Message Type + Hop Count
+ Link Address + Peer Address + Options

Options 中必含 Relay Message Option

Options 携带关键信息

IA_NA 非临时地址
含 T1 T2

IA_TA 临时地址
不含 T1 T2 保护隐私

IAID 标识该 IA

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-LLTLink-Layer + Time硬件类型 + 时间戳 + 链路层地址推荐使用,有稳定存储的设备
DUID-ENEnterprise Number企业编号(PEN)+ 厂商自定义企业/厂商设备,无链路层地址时
DUID-LLLink-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-LLTDUID-ENDUID-LL
是否推荐是(首选)企业场景备选
需要稳定存储
含时间戳
含链路层地址
接口可移除可以(需持久化)不涉及不可以
时间回绕年份2136年

选型决策流程

没有

没有

是固定接口

可拔插接口

需要生成 DUID

设备是否有稳定存储?
如磁盘 Flash 等

是否有网络接口
链路层地址?

使用 DUID-LLT
推荐首选

使用 DUID-EN
需要企业 PEN 编号

链路层地址是否来自
固定不可拔插接口?

使用 DUID-LL

不能用 DUID-LL
改用 DUID-EN

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=2321=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) 中的标志位来决定。
路由器通告中有两个关键标志位:

标志位名称含义
MManaged Address Configuration置1表示:IPv6 地址可通过 DHCPv6 获取
OOther 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无状态 DHCPv6Stateless 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(地址 + 配置)

二、完整启动流程

客户端上线后的完整步骤:

否,O=0

否,O=1

客户端启动

确定链路本地地址
fe80::xxx

ICMPv6 路由器发现
Router Discovery

收到路由器通告 RA
含 M 和 O 标志位

M 位是否为 1?

使用 SLAAC
无状态自动配置

无状态 DHCPv6
仅获取 DNS 等配置

启动 DHCPv6
组播 SOLICIT 消息

服务器位置已知
或使用 Rapid Commit?

四消息交换流程

两消息交换流程

三、四消息交换流程(标准流程)

这是 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 阶段)

DHCPv6 Server Router Client DHCPv6 Server Router Client 读取 M/O 标志位 决定启动 DHCPv6 收集响应 选择最优服务器 提交绑定 Binding=DUID+IA类型+IAID 配置完成 执行 DAD 冲突检测(推荐) ROUTER SOLICIT (ICMPv6, 寻找路由器) ROUTER ADVERTISE (ICMPv6, M=1 O=1, 使用DHCPv6) SOLICIT (组播 ff02::1:2, xid, DUID, options) ADVERTISE (xid, IP地址, 服务器ID, options) REQUEST (组播, address, 服务器ID, xid) REPLY (xid, 最终地址, options) DECLINE (若 DAD 发现冲突)

九、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,服务器重新分配
Logo

腾讯云面向开发者汇聚海量精品云计算使用和开发经验,营造开放的云计算技术生态圈。

更多推荐