IPv4 Link-Local(链路本地地址)允许设备在无 DHCP、无路由器的环境下自动配置 IP 地址(169.254.0.0/16),实现同一链路上的设备即插即用通信。
本文档描述如何在 C++ 嵌入式 Linux 环境中,基于 epoll 事件循环集成该功能。
App LinkLocal RawArpSocket Network| | | ||--start()----->| | || |--pick IP--------| || | (169.254.x.y) | || | | || |--send Probe---->| || | (ARP Who-has) |--broadcast ARP->|| | | || |<-timeout(1s)----| || | (无冲突) | || | | || |--send Probe---->| || | |--broadcast ARP->|| | | || |<-timeout(1s)----| || | (无冲突) | || | | || |--send Probe---->| || | (第3次) |--broadcast ARP->|| | | || |<-timeout(1s)----| || | (3 probes OK) | || | | || |--send GARP----->| || | (Announce) |--broadcast ARP->|| | | ||--onBound(ip)-| | || | | |
LinkLocal RawArpSocket Network App| | | ||<-recv ARP pkt---| | || (别人声明同IP) | | || | | ||--检测到冲突------>| | || | | || state=DEFENDING | | ||--send ARP Reply->| || ( defending ) |--unicast ARP--->| || | | || | 对方收到Reply | || | | || 若持续冲突 | | ||--conflict_count++| | || | | || 超过阈值 | | ||--state=PROBING | | ||--重新选IP | | ||--回到 Probe 阶段 | | |
+-------+| IDLE |+---+---+|start()|v+-----------+ 3 probes 无冲突 +-------------+| PROBING |------------------>| ANNOUNCING || 发送ARP | | 发送GARP || Who-has | | Announce |+-----+-----+ +------+------+| || 冲突(收到同IP ARP) | 2次Announce完成| |v v+-----------+ +-----------+| PROBING | | BOUND ||(重新选IP) | | 正常运行 |+-----------+ +-----+-----+|收到他人声明同IP|v+----------+| DEFENDING || 回复ARP || Reply |+----+-----+|冲突过多/超时|v+-----------+| PROBING ||(重新选IP) |+-----------+
状态说明:
| 状态 | 行为 | 超时/触发条件 |
|---|---|---|
| IDLE | 初始状态,未启动 | - |
| PROBING | 随机选 169.254.x.y,发送 3 次 ARP Probe(间隔 ~1s) | 冲突 → 重选 IP;3次无冲突 → ANNOUNCING |
| ANNOUNCING | 发送 2 次 GARP Announce(间隔 ~2s) | 完成 → BOUND |
| BOUND | 地址已绑定,正常使用 | 收到冲突 ARP → DEFENDING |
| DEFENDING | 回复 ARP Reply 声明所有权;冲突过多则退避 | 严重冲突 → 重新 PROBING |
+---------------------+ +----------------------+| EventLoop | | RawArpSocket ||---------------------| |----------------------|| + addFd(fd, cb) | | - fd_ : int || + addTimer(ms, cb) | | + open(ifindex,mac) || + run() |<>------->| + sendProbe(ip) |+---------------------+ epoll | + sendAnnounce(ip) || + recv(pkt) || + fd() : int |+----------------------+^|+----------------------+| TimerMgr ||----------------------|| - tfd_ : int || + arm(ms) || + disarm() || + fd() : int |+----------------------+^|+----------------------+| <<enum>> || LlState ||----------------------|| IDLE || PROBING || ANNOUNCING || BOUND || DEFENDING |+----------------------+^|+----------------------------------+---------------------------+| LinkLocal ||----------------------------------------------------------------|| - ifindex_ : int || - mac_[6] : uint8_t || - candidate_ : in_addr || - state_ : LlState || - probes_sent_, claims_sent_, conflict_count_ : int || - arp_ : RawArpSocket || - timer_ : TimerMgr ||----------------------------------------------------------------|| + start(onBound) || + onArpPkt(pkt) || + onTimeout() || - pickRandom169254() || - transition(s : LlState) |+--------------------------------------------------------------+
推荐基于 BusyBox zcip.c 的核心逻辑进行 C++ 封装:
# 获取 BusyBox 源码git clone https://git.busybox.net/busybox.git# 核心文件vim busybox/networking/zcip.c
改造要点:
| zcip 原语 | C++ 替代方案 |
|---|---|
read(arp_fd) 阻塞等待 | epoll EPOLLIN → recvfrom(arp_fd, ...) |
alarm() / sleep() 超时 | timerfd_settime(tfd_, ...) |
| 全局变量保存状态 | LinkLocal 类成员变量 |
fork() + script 通知 | std::function<void(in_addr)> 回调 |
linklocal/├── include/│ ├── linklocal.hpp # 对外接口│ ├── raw_arp_socket.hpp # PF_PACKET ARP 封装│ └── timer_mgr.hpp # timerfd 封装├── src/│ ├── linklocal.cpp # 状态机 + 事件回调│ ├── raw_arp_socket.cpp # PF_PACKET 原始套接字│ └── timer_mgr.cpp # timerfd 操作└── test/ └── main.cpp # epoll loop + 调用示例
linklocal.hpp:
#pragma once#include<functional>#include<cstdint>#include<netinet/in.h>class LinkLocal {public:using GotAddrCb = std::function<void(const in_addr& ll_addr)>;using LostAddrCb = std::function<void()>;explicitLinkLocal(int ifindex, constuint8_t mac[6]);~LinkLocal();// 获取需要注册到 epoll 的 fdintarpFd()const{ return arp_fd_; }inttimerFd()const{ return tfd_; }// epoll 事件回调(由 EventLoop 调用)voidonArpReadable();voidonTimerExpired();// 启动 / 停止voidstart(GotAddrCb got_cb, LostAddrCb lost_cb = nullptr);voidstop();// 查询当前状态boolhasAddress()const{ return state_ == LlState::BOUND; }in_addr currentAddress()const{ return candidate_; }private:voidpickRandom169254();voidsendArpProbe();voidsendArpAnnounce();voidsendArpReply(constuint8_t* target_mac, const in_addr& target_ip);voidtransition(LlState new_state);voidhandleConflict();// RFC 3927 参数static constexpr int PROBE_NUM = 3;static constexpr int ANNOUNCE_NUM = 2;static constexpr int PROBE_INTERVAL_MS = 1000; // 1sstatic constexpr int ANNOUNCE_INTERVAL_MS = 2000; // 2sstatic constexpr int DEFEND_THRESHOLD = 5;int ifindex_;uint8_t mac_[6];in_addr candidate_{};// 状态enum class LlState { IDLE, PROBING, ANNOUNCING, BOUND, DEFENDING } state_;int probes_sent_{0};int claims_sent_{0};int conflict_count_{0};// 底层int arp_fd_{-1};int tfd_{-1};// 回调GotAddrCb got_cb_;LostAddrCb lost_cb_;};
// main.cpp#include"linklocal.hpp"#include<sys/epoll.h>#include<unistd.h>intmain(){// 1. 创建事件循环int epfd = epoll_create1(0);// 2. 创建 LinkLocal(指定网卡 ifindex 和 MAC)uint8_t mac[6] = {0x00, 0x11, 0x22, 0x33, 0x44, 0x55};LinkLocal ll(2/* eth0 ifindex */, mac);// 3. 注册 fd 到 epollstruct epoll_event ev;ev.events = EPOLLIN;ev.data.ptr = ≪epoll_ctl(epfd, EPOLL_CTL_ADD, ll.arpFd(), &ev);ev.data.u32 = 1; // timer fd 标识epoll_ctl(epfd, EPOLL_CTL_ADD, ll.timerFd(), &ev);// 4. 启动ll.start([](const in_addr& addr) {char buf[INET_ADDRSTRLEN];inet_ntop(AF_INET, &addr, buf, sizeof(buf));printf("[LinkLocal] 获得地址: %s\n", buf);});// 5. 事件循环struct epoll_event events[8];while (true) {int n = epoll_wait(epfd, events, 8, -1);for (int i = 0; i < n; i++) {if (events[i].data.u32 == 1) {// timer fd 触发ll.onTimerExpired();} else {// ARP fd 可读ll.onArpReadable();}}}}
另外备注:
上述的方式是以源码的方式集成,其实还有更加简单的办法,如下:
① 如果系统支持dhcpcd
# /etc/dhcpcd.conf interface eth0 # 启用 IPv4 Link-Local(默认已启用,显式声明更清晰) ipv4ll # DHCP 超时时间(秒) timeout 10 # DHCP 失败后自动 fallback 到 Link-Local noipv4ll=no # 可选:DHCP 成功时删除 Link-Local(避免双 IP) # 如果希望两者共存,注释掉这行 release # DHCP 获取后释放 Link-Local
② BusyBox + 手动脚本 ---- 我们有个Android 项目,就是使用的这种脚本的办法
#!/bin/sh # /usr/local/bin/link-local.sh IFACE="eth0" LL_PREFIX="169.254" generate_addr() { local o3=$((RANDOM % 254 + 1)) local o4=$((RANDOM % 254 + 1)) echo "$LL_PREFIX.$o3.$o4" } check_conflict() { local addr=$1 # arping -c 2 -w 2 -I $IFACE $addr # 返回 0 = 有响应(冲突),非 0 = 无响应(可用) arping -c 2 -w 2 -I "$IFACE" "$addr" >/dev/null 2>&1 return $? } claim_addr() { local addr=$1 ip addr add "$addr/16" brd "$LL_PREFIX.255.255" dev "$IFACE" ip link set "$IFACE" up echo "Link-Local: $addr" } # 主逻辑 while true; do ADDR=$(generate_addr) if ! check_conflict "$ADDR"; then claim_addr "$ADDR" break fi sleep 1 done 启动方式: # /etc/init.d/S40linklocal #!/bin/sh case "$1" in start) /usr/local/bin/link-local.sh & ;; stop) ip addr del 169.254.0.0/16 dev eth0 2>/dev/null ;; esac
// ARP 帧格式(以太网 + ARP)structEthArpPkt {// Ethernet headeruint8_tdst_mac[6]; // 00:00:00:00:00:00 (广播) / 目标MACuint8_tsrc_mac[6]; // 本机MACuint16_teth_type; // 0x0806 (ETH_P_ARP)// ARP payloaduint16_thw_type; // 0x0001 (Ethernet)uint16_tproto_type; // 0x0800 (IPv4)uint8_thw_size; // 6uint8_tproto_size; // 4uint16_topcode; // 1=Request, 2=Replyuint8_tsender_mac[6]; // SPA 对应MACuint8_tsender_ip[4]; // SPA (Probe时为0.0.0.0)uint8_ttarget_mac[6]; // TPA 对应MACuint8_ttarget_ip[4]; // TPA (Probe目标IP)} __attribute__((packed));
| 参数 | 值 | 说明 |
|---|---|---|
| 地址范围 | 169.254.0.0/16 | 排除 169.254.0.0/24 和 169.254.255.0/24 |
| Probe 次数 | 3 | 每次间隔 1s(随机 ±0~500ms) |
| Announce 次数 | 2 | 每次间隔 2s |
| Probe 等待超时 | 1s | 无回复视为无冲突 |
| Defend 阈值 | 可配置,建议 5 | 超过后放弃地址重新选 |
权限:PF_PACKET 原始套接字需要 CAP_NET_RAW 权限(或 root)
许可证:BusyBox 为 GPLv2,改造后需遵守 GPL 条款;如需闭源可参考实现逻辑自行编写
并发:同一链路多个设备同时启动会随机碰撞,RFC 3927 的随机退避已处理此场景
与 DHCP 共存:建议 DHCP 获取成功时主动释放 Link-Local 地址
嵌入式裁剪:可去掉 ANNOUNCING 状态,Probe 通过后直接 BOUND,减少延迟