Openwrt DHCP Relay
1、功能
2、工作原理
3、测试
4、报文分析

1. 客户端发送 DHCP Discover(广播)
-
客户端启动后,在本地子网内广播发送 DHCP Discover 消息(目标地址 255.255.255.255,源地址 0.0.0.0)。
-
此消息的目的是寻找可用的 DHCP 服务器。
2. 中继代理接收广播
-
中继代理(如路由器)监听到客户端的 DHCP 广播请求。
-
由于路由器默认会隔离广播域(不转发广播包到其他子网),因此需要中继代理介入。
3. 中继代理转发请求(单播)
-
中继代理将 DHCP Discover 消息修改为 单播,并转发到预先配置的 DHCP 服务器地址。
-
修改后的数据包包含以下关键信息:
-
目标地址:DHCP 服务器的 IP 地址(单播)。
-
源地址:中继代理的接口 IP 地址(即客户端所在子网的网关 IP)。
-
新增字段 giaddr:中继代理将客户端的子网网关 IP(Gateway IP Address)填入 giaddr 字段,告知服务器客户端所属子网。
4. DHCP 服务器处理请求(单薄)
-
服务器收到请求后,根据 giaddr 字段确定客户端所在的子网。
-
从对应子网的地址池中分配 IP 地址,生成 DHCP Offer 消息。
-
服务器将 DHCP Offer 发送给中继代理(目标地址为中继代理的 IP)。
5. 中继代理转发 Offer 到客户端
-
中继代理将 DHCP Offer 消息转发回客户端所在的子网(广播或单播,具体取决于网络配置)。
6. 客户端发送 DHCP Request(广播)
-
客户端收到 Offer 后,广播 DHCP Request 消息,确认选择的 IP 地址。
7. 中继代理转发 Request 到服务器
-
中继代理再次将 DHCP Request 单播转发给 DHCP 服务器。
8. 服务器发送 DHCP ACK
-
服务器确认请求,发送 DHCP ACK 消息(包含 IP 地址、租期等信息)到中继代理。
9. 中继代理转发 ACK 到客户端
-
中继代理将 DHCP ACK 转发给客户端,完成 IP 分配。
注意:
-
中继代理的核心作用:将客户端的广播转换为单播,实现跨子网的DHCP通信。
-
仅在客户端子网内使用广播:客户端初始请求和中继代理的最终响应可能使用广播,但中继代理与服务器之间必须单播。
5、代码解析
概述



启动流程
# auto-generated config file from /etc/config/dhcp
conf-file=/etc/dnsmasq.conf
dhcp-authoritative
domain-needed
localise-queries
read-ethers
enable-ubus=dnsmasq
expand-hosts
bind-dynamic
local-service
all-servers
edns-packet-max=1232
domain=lan
local=/lan/
server=/10.51.100.215/202.96.134.133@pon1.200.1
server=/10.51.100.215/152.152.152.222@pon1.200.1
server=/172.18.18.xxx/202.96.134.133@pon1.200.1
server=/172.18.18.xxx/152.152.152.222@pon1.200.1
addn-hosts=/tmp/hosts
dhcp-leasefile=/tmp/dhcp.leases
resolv-file=/tmp/resolv.conf.d/resolv.conf.auto
dhcp-broadcast=tag:needs-broadcast
conf-dir=/tmp/dnsmasq.d
user=dnsmasq
group=dnsmasq
dhcp-ignore-names=tag:dhcp_bogus_hostname
conf-file=/usr/share/dnsmasq/dhcpbogushostname.conf
dhcp-relay=192.168.29.1,172.18.18.215,pon1.200.1
bogus-priv
conf-file=/usr/share/dnsmasq/rfc6761.conf
dhcp-range=set:lan,192.168.29.100,192.168.29.249,255.255.255.0,86400s
│ main()入口 │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 初始化阶段 │
├─解析命令行参数 │
├─读取配置文件 │
├─初始化全局结构体daemon│
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 服务初始化 │
├─ DNS模块初始化 │ // check_dns_listeners()
│ (cache_init等) │
├─ DHCP模块初始化 │ // dhcp_init()
├─ TFTP模块初始化 │ // check_tftp_listeners()
├─ 网络接口绑定 │ // create_bound_listeners()
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 权限处理 │
├─ 能力机制(Capabilities│ // capset()
├─ 降权运行(setuid) │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 主事件循环 │
├───────────────────────┤
│ for(;;) { │
│ poll()监听所有fd │ // do_poll()
│ ├─DNS请求处理 │ <── daemon->dnsfd(s)
│ ├─DHCP包处理 │ <── daemon->dhcpfd
│ ├─TFTP传输处理 │ <── daemon->tftpfd
│ └─系统事件处理 │ <── piperead/信号处理
│ } │
└───────────────────────┘
└─ 更新租约文件 → 触发DNS更新
DNS模块 ───┬─ 响应查询请求
└─ 支持DNSSEC验证
TFTP模块 ──┐
└─ 独立传输通道
DHCP relay的核心函数:
完整工作流程
-
请求接收:通过dhcp_packet()接收客户端请求
-
中继标识:
-
设置giaddr为本地中继IP
-
增加hops计数器防止环路
-
服务器转发:通过relay_upstream4()向预配置的上游服务器转发请求
-
响应接收:通过relay_reply4()验证响应合法性
-
接口匹配:检查到达接口是否匹配中继配置
-
响应回传:通过原始接收接口返回响应给客户端
1. 请求转发阶段(relay_upstream4函数)
{
// 检查是否为合法DHCP请求
if (mess->op != BOOTREQUEST) return 0;
// 防止环路(检查giaddr是否已设置)
if (mess->giaddr.s_addr) {
if (mess->giaddr.s_addr == relay->local.addr4.s_addr)
return 1; // 检测到环路
} else {
mess->giaddr.s_addr = relay->local.addr4.s_addr; // 设置中继地址
}
// 跳数限制
if ((mess->hops++) > 20) return 1;
// 遍历所有中继配置
for (; relay; relay = relay->current) {
// 构造目标地址(DHCP服务器)
to.in.sin_addr = relay->server.addr4;
send_from(daemon->dhcpfd, 0, (char *)mess, sz, &to, &from, 0);
// 记录中继日志
my_syslog(MS_DHCP | LOG_INFO, _("DHCP relay %s -> %s"),...);
// 保存接口索引用于响应处理
relay->iface_index = iface_index;
}
return 1;
}
2. 响应回传阶段(relay_reply4函数)
{
// 验证响应有效性
if (mess->giaddr.s_addr == 0 || mess->op != BOOTREPLY)
return NULL;
// 遍历所有中继配置
for (relay = daemon->relay4; relay; relay = relay->next) {
// 匹配网关地址和接收接口
if (mess->giaddr.s_addr == relay->local.addr4.s_addr &&
(!relay->interface || wildcard_match(relay->interface, arrival_interface))) {
return relay->iface_index != 0 ? relay : NULL;
}
}
return NULL;
}
整个DHCP 服务请求实现流程:dhcp.c
│ 接收DHCP数据包 │
│ (recv_dhcp_packet) │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 接口验证与预处理 │
├─ 检查接收接口有效性 │ // indextoname()
├─ 处理桥接接口别名 │ // --bridge-interface
├─ 判断回环接口 │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 中继回复处理分支 │
│ (relay_reply4判断) │
└──────────┬────────────┘
├─是─→ 设置中继接口索引
│ 准备原始响应转发
↓
┌───────────────────────┐
│ 本地请求处理分支 │
├─ 检查接口IP有效性 │ // ioctl(SIOCGIFADDR)
├─ 过滤例外接口 │ // --dhcp-ignore
├─ 初始化上下文链 │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 地址匹配与上下文构建 │
├─ 主/备地址匹配 │ // check_listen_addrs()
├─ 构建可用地址池 │ // complete_context()
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 中继决策点 │
│ (parm.relay_local判断)│
└──────────┬────────────┘
├─需中继─→ 转发上游服务器
│ // relay_upstream4()
↓
┌───────────────────────┐
│ 本地地址分配处理 │
├─ 租约清理 │ // lease_prune()
├─ 生成DHCP回复 │ // dhcp_reply()
├─ 更新租约文件 │ // lease_update_file()
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 响应包构造与发送 │
├─ 设置目标地址 │
│ (giaddr/ciaddr等) │
├─ 平台相关发送处理 │
│ (ARP缓存/Linux/BSD) │
└──────────┬────────────┘
↓
┌───────────────────────┐
│ 错误处理与日志记录 │
│ my_syslog()记录异常 │
└───────────────────────┘
实际上DHCP 请求的四步:

