OpenWrt 动态绑定 ARP 脚本

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地址>,即可实现我们所需要达到的目的
登录查看完整内容

发表回复