OpenWrt 默认没有绑定 ARP 的功能,难道这功能真没什么卵用?或者是这功能应该在交换机上部署,不太合适在路由上搞?可是有绑定 ARP 功能的交换机那个价格应该不是我们家用可以接受的吧?
实际上家庭局域网真没什么必要绑定 ARP ,因为都是自家设备也没几台,即使存在 ARP 攻击,也很容易揪出是哪台设备在搞事情吧。
如果只在路由上绑定,效果也不太好,还需在设备上再绑定路由的IP与MAC,双向绑定更完美。唯一的遗憾就是双向绑定真的比较麻烦,而且移动设备更麻烦且可能无法实现。
即使是这么苛刻的条件,可是我还是折腾了几天,迭代了几个版本哟!终于搞了一个 OpenWrt 的脚本,OpenWrt 在提供 DHCP 服务的时候触发脚本,实现自动绑定静态租约的主机。
OpenWrt 动态绑定 ARP 脚本 v3.6.2
注释的英文是用中文机翻出来的,目的不是装13,而是为了更好的兼容性。
需要安装 ip-full 才能使用 ip neigh replace 修改 ARP 状态。
最后,在 /etc/dnsmasq.conf 上增加一项配置实现在提供 DHCP 服务时,触发该脚本实现绑定 ARP。
dhcp-script=/etc/fz-bind-arp.sh
注释这行配置项,即可 停用该脚本。
脚本迭代过程及遇到的各种问题
最初构想的功能很简单,只要 OpenWrt 能够将 DHCP 静态租约主机 IP与MAC 绑定为静态 ARP 表即可。在实现这个简单的功能的过程可没这么简单…
初始准备工作
在 OpenWrt 上,我们无法使用 arp 命令修改 ARP 表的状态,只有使用 ip neigh 命令可以实现本次简单任务所需达到的目的。关于使用这个命令的各种操作,这里不做详细记录。
本次简单任务,实际上就是修改 ARP 表上对应的 IP与MAC 的状态为 PERMANENT 即达到了静态绑定的目的。
特别注意:我们修改状态为 PERMANENT (0x6)之后,如果租约到期或其他因素主机重新上线,ARP 表的状态会重新刷新为 REACHABLE (0x2),此时我们还得再次修改为 PERMANENT (0x6)达到静态绑定
我们先了解一下 ARP 表的各种状态
使用 ip neigh show 命令
root@OpenWrt:~# ip neigh show 192.168.33.30 dev br-lan FAILED 192.168.33.85 dev br-lan lladdr 34:a8:eb:4d:ec:51 PERMANENT 192.168.33.22 dev br-lan lladdr bc:ad:28:3d:bf:0f STALE 192.168.33.201 dev br-lan lladdr 22:a9:66:da:ab:61 DELAY 192.168.33.21 dev br-lan lladdr c0:51:7e:88:e7:a1 REACHABLE 192.168.33.90 dev br-lan lladdr 50:0f:f5:2f:ff:00 PERMANENT
使用 arp -a 命令 或 cat /proc/net/arp
root@OpenWrt:~# cat /proc/net/arp IP address HW type Flags HW address Mask Device 192.168.33.30 0x1 0x0 00:00:00:00:00:00 * br-lan 192.168.33.85 0x1 0x6 34:a8:eb:4d:ec:51 * br-lan 192.168.33.22 0x1 0x2 bc:ad:28:3d:bf:0f * br-lan 192.168.33.201 0x1 0x2 22:a9:66:da:ab:61 * br-lan 192.168.33.21 0x1 0x2 c0:51:7e:88:e7:a1 * br-lan 192.168.33.90 0x1 0x6 50:0f:f5:2f:ff:00 * br-lan
其中 arp -a 查看的 ARP 表,Flags 这一列
- 0x0 表示离线
- 0x6 表示静态
- 0x2 表示在线
初始构思版
开始的时候我们将脚本配置在 启动项 → 本地启动脚本,只是实现了 OpenWrt 启动的时候自动运行一次脚本,但发现如果租约到期或其他因素主机重新上线,ARP 表的状态会重新刷新为 REACHABLE (0x2)
于是使用定时任务的方式运行脚本来实现绑定 ARP。那么如何定义运行脚本的时间间隔?每10分钟,或每30分钟,或每1小时 … ?
- 定义运行脚本的时间间隔越短,绑定效率越高效果越好,但越耗费硬件性能,不可取!
- 定义运行脚本的时间间隔越长,绑定效率越差效果不理想,但越节省硬件资源。还行吧。
于是我们取了一个比较合适的时间间隔:20分钟运行一次脚本。
有没有注意到脚本文件第一行 #!/bin/sh 而不是 #!/bin/bash
在 OpenWrt 上的脚本我们可以直接使用命令 sh /PATH/custom.sh 来运行第一行声明 #!/bin/bash 的脚本文件,但却不能直接 /PATH/custom.sh 这样运行第一行声明 #!/bin/bash 的脚本文件,却可以直接 /PATH/custom.sh 这样运行第一行声明 #!/bin/sh 的脚本文件,表达得有点绕!具体关于这个问题请自行搜索相关的资料。
#!/bin/sh
# Bind Static leases ARP - sgtfz 202204170530
# 在 /tmp/etc/ 这个目录下的 dnsmasq.conf.cfg01411c
# 这个文件是 Openwrt 系统启动时自动生成的
# 由于我对这个文件名的不确定性,因此采用 grep 匹配文件名前缀 dnsmasq.conf
# 命令很粗鲁,不严谨吧
static_leases=$(cat /tmp/etc/$(ls /tmp/etc/ | grep "dnsmasq.conf") | grep dhcp-host | sed 's/dhcp\-host\=//')
# 首次学会了使用 for in 循环
for fzbind in $static_leases; do
host_mac=$(echo $fzbind | awk -F "," '{print $1}')
host_ip=$(echo $fzbind | awk -F "," '{print $2}')
# 没发现 ip neigh replace 命令前,采用下列拙劣的做法
# 如果 ARP 表存在的条目就无法再使用 ip neigh add 添加的。
# 因此使用 || 实现如果前面命令执行失败,才会执行后面的命令
ip neigh add $host_ip lladdr $host_mac nud permanent dev br-lan || ip neigh change $host_ip lladdr $host_mac nud permanent dev br-lan
done
但我一直在想,如果能够在 DHCP 分配 IP 地址时,触发脚本绑定 ARP ,那不是太完美了?可是这么刁钻的方式切入,可能吗?于是一顿操作猛如虎的搜索了关于这种方式案例,可是貌似真没有如此刁钻的案例可供参考。
功夫不负有心人啊,后来我在 OpenWrt 官方文档中发现 /etc/dnsmasq.conf 这个配置文件上可增加触发自定义脚本的配置项:
dhcp-script=/PATH/custom.sh
这简直太完美啦,最想要的就是这种方式触发脚本!
还可以在 /etc/config/dhcp 这个配置文件增加配置项实现触发自定义脚本:
config dnsmasq option dhcpscript '/PATH/custom.sh'
但我发现如果在 /etc/config/dhcp 这个配置文件上配置的话,在重启 dnsmasq 时会瞬间触发10多20次脚本运行,即使配置自定义脚本在 /etc/dnsmasq.conf 配置文件上也会在 dnsmasq 重启时瞬间触发明显更少(8次左右)的脚本运行。
动态绑定 ARP 初始版
这个版本是直接获取 DHCP 静态租约的主机,提取到 IP与MAC,进行修改 ARP 状态来实现绑定。
存在太多的 bug:
- 只要DHCP静态租约配置列表主机,都会添加到静态ARP表
- 即使实际不存在的或离线的主机,都会添加到静态ARP表
- 每次触发脚本都会进行 ip neigh replace 操作替换静态ARP表
- 脚本执行没有记录任何日志可查询
- 更多的 bug 可能我还没有发现 …
#!/bin/sh
# Bind Static leases ARP - sgtfz 202204171905
# 因为由 dnsmasq 在提供 DHCP 服务时触发脚本
# 如果在 dnsmasq 重启时会连续触发8次左右,因此我们加入休息时间 10秒
sleep 10
# 简单逻辑为 获取DHCP静态租约的主机,提取到 IP与MAC
static_leases=$(cat /tmp/etc/$(ls /tmp/etc/ | grep "dnsmasq.conf") | grep dhcp-host | sed 's/dhcp\-host\=//')
# 判断静态租约不为空
if [ -n "$static_leases" ]; then
# 使用 for in 循环配合 ip neigh replace 命令绑定 ARP
for fzbind in $static_leases; do
host_mac=$(echo $fzbind | awk -F "," '{print $1}')
host_ip=$(echo $fzbind | awk -F "," '{print $2}')
# 使用 ip neigh replace 命令即可,简洁了不少。
ip neigh replace $host_ip lladdr $host_mac nud permanent dev br-lan
done
fi
动态绑定 ARP v3
在初始版的前提下,看到了方案的可行性。那么需要完善的地方有许多,经过各种折腾后,来到了个人认为比较完善的 v3 版本了。
#!/bin/sh
# Bind static lease to router ARP table
# SGTfz 2022-4-19
# fz-bind-arp-v3-beta
# 依然还是提取 DHCP 静态租约列表的主机 IP与MAC
# Static Leases IP & MAC
static_addr=$(grep dhcp-host= /tmp/etc/dnsmasq.conf.* | awk -F "[=,]" -v OFS="," '{print $3,$2}')
# 加入一个新的变量,用作与 DHCP 静态租约列表的主机 IP与MAC 作为比对
# 获取 ARP 表上已静态绑定的主机 IP与MAC
# In ARP Bound IP & MAC
bound_addr=$(ip neigh show | grep PERMANENT | awk -v OFS="," '{print $1,$5}')
# 这样去重的方法有个严重的 bug 后续会说明。
# 排除重复的 IP与MAC 的行,留下不重复的行,得到我们需要绑定的 IP与MAC 了。
# Unbound IP & MAC
diff_ip=$(echo -e "$static_addr\n$bound_addr" | sort | uniq -iu)
# 如果有需要绑定的 IP与MAC ,我们仅修改未绑定的条目,节省无谓的资源浪费。
if [ -n "$diff_ip" ]; then
# 执行 for in 循环修改 ARP 表达到绑定目的
for item in $diff_ip; do
host_mac=$(echo $item | awk -F "," '{print $2}')
host_ip=$(echo $item | awk -F "," '{print $1}')
ip neigh replace $host_ip lladdr $host_mac nud permanent dev br-lan
done
# 加入日志输出到文件,这个方法不太好,后来我们使用 logger 输出日志了。
echo -e "\n$(date "+%Y-%m-%d %H:%M:%S")\n$diff_ip\nBinding is complete.\n" >> /tmp/fz-bind-arp.log
else
# 加入日志输出到文件,这个方法不太好,后来我们使用 logger 输出日志了。
echo -e "\n$(date "+%Y-%m-%d %H:%M:%S") Great! All static lease have been bound.\n" >> /tmp/fz-bind-arp.log
fi
动态绑定 ARP v3.2
当认真的做一件事情之后,就会尽可能的让这件付出了努力的事情变得更完美。但越是追求完美,越发现越多地方需要进一步的去完善 …
#!/bin/sh
# Bind static lease IP&MAC to router ARP table
# SGTfz 2022-4-20
# fz-bind-arp-v3.2-beta
sleep 3
# Static Leases IP&MAC
static_addr=$(grep dhcp-host= /tmp/etc/dnsmasq.conf.* | awk -F "[=,]" -v OFS="," '{print $3,$2}')
# 我们考虑到 ip neigh show 命令可能更为消耗系统资源
# 所以更改为直接匹配文件的方式。grep 0x6 /proc/net/arp
# In ARP IP&MAC
bound_addr=$(grep 0x6 /proc/net/arp | awk -v OFS="," '{print $1,$4}')
# Or Use <ip neigh show> command, But I think this command consumes more processor.
# bound_addr=$(ip neigh show | grep PERMANENT | awk -v OFS="," '{print $1,$5}')
# 下面我们说说这行命令的在此场景下的致命性 bug
# 如果我们修改静态绑定租约从原IP:192.168.33.88 MAC XX:XX:XX:XX:xX:XX
# 修改为新的IP: 192.168.33.99
# 这行命令会找出新IP:192.168.33.99,MAC XX:XX:XX:XX:xX:XX ,然后执行for in循环绑定
# 然鹅绑定了新的IP:192.168.33.99,MAC XX:XX:XX:XX:xX:XX,之后
# 旧的IP:192.168.33.88 MAC XX:XX:XX:XX:xX:XX,又被找出了,然后执行for in循环绑定
# 由于旧的IP还存在于 ARP 表上处于静态绑定状态
# 即使主机离线还会保留一段时间,或直到主机重新上线获得新的IP地址后,旧的条目才会消失
# 于是致命的进入了死循环,一直匹配到旧的:192.168.33.88 MAC XX:XX:XX:XX:xX:XX
# 一直执行for in循环绑定,直到这台主机离线许久或重新获得新IP地址,才会终止死循环。
# Find static leases IP&MAC not in ARP table
diff_ip=$(echo -e "$static_addr\n$bound_addr" | sort | uniq -iu)
# Bind IP&MAC to router ARP table using <ip neigh replace> command.
if [ -n "$diff_ip" ]; then
for item in $diff_ip; do
host_mac=$(echo $item | awk -F "," '{print $2}')
host_ip=$(echo $item | awk -F "," '{print $1}')
ip neigh replace $host_ip lladdr $host_mac nud permanent dev br-lan
done
# 学会了使用 logger 输出日志了,在 OpenWrt 系统日志便可查询该脚本的日志
logger -t fz-bind-arp "Binding is complete $diff_ip"
else
# 学会了使用 logger 输出日志了,在 OpenWrt 系统日志便可查询该脚本的日志
logger -t fz-bind-arp "Nothing to do"
fi
sleep 2
动态绑定 ARP v3.3
越是深究,越是深不见底,此时的我已经无计可施了,能力与时间都有限,只能输出文件作为匹配的规则文件来修复上一个版本的致命 bug
#!/bin/sh
# Bind static lease IP&MAC to router ARP table
# SGTfz 2022-4-22
# fz-bind-arp-v3.3-beta
sleep 2
# Static Leases IP&MAC
static_addr=$(grep dhcp-host= /tmp/etc/dnsmasq.conf.* | awk -F "[=,]" -v OFS="," '{print $3,$2}')
# ARP table IP&MAC
bound_addr=$(grep 0x6 /proc/net/arp | awk -v OFS="," '{print $1,$4}')
# 还保留这行命令,是为了在已绑定所有静态租约的地址后
# 空闲时节省系统资源,不必每次都进入执行输出规则文件的步骤
# Find out the difference IP&MAC from static leases and ARP table.
diff_addr=$(echo -e "$static_addr\n$bound_addr" | sort | uniq -iu)
if [ -n "$diff_addr" ]; then
# 输出文件,作为匹配使用的规则文件,尽可能将文件存储在内存,读取速度更快?
# Output file as matching rules
grep 0x6 /proc/net/arp | awk -v OFS="," '{print $1,$4}' > /dev/shm/arp_bound_addr.txt
# 此处我们用到 grep 的文件比对,所以才需要输出一个文件作为比对的规则文件
# grep -ivf <文件A(规则文件)> <文件B(匹配并删除与规则文件重复的行,留下不重复的行)>
# 获得静态租约的IP与MAC,然后通过管道送到后面配置规则文件
# 找出静态租约里与ARP表上不重复的行。
# 这是作为比对的规则文件</dev/shm/arp_bound_addr.txt>
# Remove the line where the static lease is the same as the bound address,
# keep static lease distinct rows.
static_chg=$(grep dhcp-host= /tmp/etc/dnsmasq.conf.* \
| awk -F "[=,]" -v OFS="," '{print $3,$2}' | grep -ivf /dev/shm/arp_bound_addr.txt)
if [ -n "$static_chg" ]; then
for item in $static_chg; do
host_mac=$(echo $item | awk -F "," '{print $2}')
host_ip=$(echo $item | awk -F "," '{print $1}')
ip neigh replace $host_ip lladdr $host_mac nud permanent dev br-lan
done
logger -t fz-bind-arp "Binding is complete: $static_chg"
else
logger -t fz-bind-arp "Static lease changed, Old IP: $diff_addr still in ARP table"
fi
else
logger -t fz-bind-arp "Nothing to do"
fi
sleep 1
然后,又发现这个版本的命令还是有许多不够完善的地方,但是只要你继续纠结下去,貌似一直都可以再完善下去 …
还需完善的地方:
- 我觉得不应该输出文件比较好吧?即使是存储到内存的规则文件。
- 不想使用
grep -ivf据说貌似使用grep -ivf匹配规则有点费性能资源。 - 还待解决的问题有:
- 如果新添加一台在线主机IP:192.168.33.105 到静态租约为IP:192.168.33.55
- 该主机只有下线再重新上线才会重新获得静态租约配置的新IP:192.168.33.55
- 该主机在没有下线重新获得IP地址之前,将还在使用旧IP:192.168.33.105
- ARP表里会有2个该主机的IP的静态记录分别是:192.168.33.55与192.168.33.105
- 因为新修改的静态租约IP被添加到ARP表,旧IP的ARP表记录仍在使用或没失效。
- 脚本只会添加静态租约上的IP与MAC到ARP表,而不管主机处于在线或离线。
- 还有我没有发现的 …
动态绑定 ARP v3.6.1
我想这是稳定版了,如此简单的一个脚本,就不应该再纠结太多了。
- 实现了判断待绑定的地址的主机是否在线,离线主机不绑定。
- 取消
grep -ivf命令。那么就肯定不用输出规则文件了。 - 修复了新添加静态租约或修改静态租约地址,ARP表绑定两个IP地址的情况。
脚本的各行命令就不再详细说明了,有点累了。
动态绑定 ARP v3.6.2
在上一个版本的基础上,再进一步精简了不必要的命令行,尽量的节省资源,因为我发觉即使是在提供 DHCP 服务时触发脚本,在白天多人链接 WiFi 的时段,还是挺频繁的触发脚本的。
偶尔静下心的细嚼慢咽上一版本总觉得还可以精简,才发现之前为何如此糊涂!
仅需比对两个文件:
- 直接比对
<静态租约地址>与<ARP表状态为0x2地址>,即可实现我们所需要达到的目的

