Openwrt DHCP Relay

1、功能

解决跨子网的DHCP请求问题
 
在lan中,客户端与dhcp服务器位于不同子网时,客户端发出的dhcp广播请求无法直达服务器
dhcp relay的作用:
1)转发跨子网请求:将客户端dhcp广播转换为单播,并跨子网转发至指定的dhcp服务器
2)回传服务器响应:将dhcp服务器的响应报文(如offer,ack等)回传给客户端,确保其成功获取ip地址
 

2、工作原理

1)客户端广播:客户端发送discover,但广播无法跨越子网
2)中继代理接入:
中继代理(通常在路由器或三层交换机)监听到客户端的广播请求
将广播包修改为单播,添加**网关ip(giaddr字段)**标识客户端所在子网
将请求转发至预先配置的dhcp服务器
3)服务器处理:
dhcp服务器根据giaddr确定客户端子网,从对应的地址池分配ip
响应报文通过中继代理返回客户端
 

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、代码解析

概述

dnsmasq是openwrt的一个轻量级的多功能服务工具,主要用于提供DNS功能,维护dns信息和DHCP服务
 
tftp32无法成功分配ip的原因:
服务器offer回复携带ciaddr时的处理:
dhcp数据包处理函数:dhcp_packet:服务器回复offer报文携带ciaddr数据时(此举不合规范),如下图所示,按照代码匹配会试图将offer报文转发到ciaadr携带的ip地址,实际无法匹配,无法转发
这里判断逻辑的实际使用场景:
1、客户端处于初始化无IP状态时使用广播
2、客户端已有ip时使用单播
3、通过中继代理时保持地址一致性

启动流程

start_service函数里面会读取/etc/config/dhcp和/etc/config/networek下面的配置文件,然后集成出一份新的配置文件
/var/etc/dnsmasq.conf.cfg01411c
US@OpenWrt:~# cat /var/etc/dnsmasq.conf.cfg01411c

# 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

dnsmasq中main函数实现:
┌───────────────────────┐

│    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/信号处理

│ }                    │

└───────────────────────┘

单一进程+多路复用,各模块通过daemon全局结构体共享配置:
DHCP模块 ──┬─ 处理DISCOVER/REQUEST

            └─ 更新租约文件 → 触发DNS更新

           

  DNS模块 ───┬─ 响应查询请求

            └─ 支持DNSSEC验证

           

  TFTP模块 ──┐

            └─ 独立传输通道

DHCP relay的核心函数:

完整工作流程

  1. 请求接收:通过dhcp_packet()接收客户端请求
  2. 中继标识:
    • 设置giaddr为本地中继IP
    • 增加hops计数器防止环路
  3. 服务器转发:通过relay_upstream4()向预配置的上游服务器转发请求
  4. 响应接收:通过relay_reply4()验证响应合法性
  5. 接口匹配:检查到达接口是否匹配中继配置
  6. 响应回传:通过原始接收接口返回响应给客户端

1. 请求转发阶段(relay_upstream4函数)

static int relay_upstream4(struct dhcp_relay *relay, struct dhcp_packet *mess, size_t sz, int iface_index)

{

    // 检查是否为合法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函数)

static struct dhcp_relay *relay_reply4(struct dhcp_packet *mess, char *arrival_interface)

{

    // 验证响应有效性

    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 请求的四步:

上面的dhcp网段的获取范围为:/var/etc/dnsmasq.conf.cfg01411c
其设备列表被写入:
 
posted @ 2025-04-03 11:19  Amelia_zhou  阅读(383)  评论(0)    收藏  举报