优化OPNSense中高CPU占用的DHCP未知设备检测Shell脚本
问题
我在OPNSense中运行以下Shell脚本,用于检测与静态表不匹配的新DHCP分配并发送告警邮件,但该脚本CPU占用过高。我考虑仅对比指定时间段内的日志变化,请问有更好的优化方案吗?
原脚本:
#!/usr/local/bin/bash # Define the location of the static DHCP release file STATIC_DHCP_FILE="/var/dhcpd/etc/dhcpd.conf" # Define the location of the new DHCP release file NEW_DHCP_FILE="/var/log/dhcpd/latest.log" # Define the subject of the email SUBJECT="New DHCP Release Detected" # Define the last line of the file LAST_LINE=$(tail -n 1 "$NEW_DHCP_FILE") # Create last line file touch /tmp/last_match while true; do # Check if the file has changed if ! cmp -s /tmp/last_match "$NEW_DHCP_FILE"; then # Use cmp instead of diff LAST_LINE=$(tail -n 1 "$NEW_DHCP_FILE") DHCP_MODE=$(echo "$LAST_LINE" | awk '{print $9}') if [ "$DHCP_MODE" == "DHCPOFFER" ]; then IP=$(echo "$LAST_LINE" | awk '{print $11}') MAC=$(echo "$LAST_LINE" | awk '{print $13}') ETH=$(echo "$LAST_LINE" | awk '{print $15}') if ! grep -qi "$MAC" "$STATIC_DHCP_FILE"; then # Run the nmap command and store the output in a variable OUTPUT=$(nmap -sP "$IP") # Extract the hostname from the output using grep and cut HOSTNAME=$(echo "$OUTPUT" | grep -m 1 "Nmap scan report for" | cut -d " " -f 5) echo "$LAST_LINE" > /tmp/last_match BODY=`printf "A new DHCP release has been detected for an unknown device. The details are as follows:\n\nOn $(date)\nIP Address: $IP\nMAC Address: $MAC\nHostname: $HOSTNAME\n"` echo -e "$BODY" sleep 3 python3 /root/sendmail.py -s "$SUBJECT" -a "$BODY" fi fi fi done
优化方案
原脚本CPU占用高的核心原因是无限循环无间隔轮询,以及每次轮询都用cmp对比整个日志文件,加上冗余的进程调用(多次echo+awk)。以下是针对性优化:
- 用
tail -F实时跟踪日志,替代轮询对比:tail -F会持续监听日志文件的新增内容,只有当有新行写入时才触发处理,彻底消除无意义的轮询,大幅降低CPU占用。 - 预加载静态MAC列表,避免重复读取:把静态DHCP配置中的MAC地址提前提取并存入内存数组,后续检查时直接对比,避免每次都
grep整个配置文件。 - 合并进程调用,减少资源消耗:原脚本多次用
echo | awk提取字段,改成单次awk处理一行日志,减少进程创建开销。 - 优化主机名获取逻辑:用更轻量的
arping替代nmap获取主机名,若失败再降级使用nmap,或直接用IP作为兜底。 - 新增冷却机制,避免重复告警:设置固定冷却时间,同一设备短时间内多次触发DHCPOFFER时,不重复发送邮件。
优化后的脚本
#!/usr/local/bin/bash STATIC_DHCP_FILE="/var/dhcpd/etc/dhcpd.conf" NEW_DHCP_FILE="/var/log/dhcpd/latest.log" SUBJECT="新DHCP分配告警" LAST_CHECK_FILE="/tmp/last_dhcp_check" COOLDOWN=300 # 5分钟冷却时间,避免重复告警 # 预加载静态MAC地址到数组 declare -A STATIC_MACS while read -r line; do if [[ "$line" =~ hardware\ ethernet\ ([0-9a-fA-F:]+); ]]; then MAC=$(echo "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]') STATIC_MACS["$MAC"]=1 fi done < "$STATIC_DHCP_FILE" # 处理单条DHCP日志行的函数 process_log_line() { local line="$1" # 用awk一次提取所有需要的字段 read -r DHCP_MODE IP MAC ETH <<< "$(echo "$line" | awk '{print $9, $11, $13, $15}')" if [[ "$DHCP_MODE" != "DHCPOFFER" ]]; then return fi # 转成小写避免大小写匹配问题 MAC=$(echo "$MAC" | tr '[:upper:]' '[:lower:]') # 检查是否在静态列表中 if [[ -z "${STATIC_MACS[$MAC]}" ]]; then # 检查是否在冷却期内 if [[ -f "$LAST_CHECK_FILE" ]]; then LAST_TIME=$(cat "$LAST_CHECK_FILE") CURRENT_TIME=$(date +%s) if [[ $((CURRENT_TIME - LAST_TIME)) -lt $COOLDOWN ]]; then return fi fi # 优先用arping获取主机名(更轻量) HOSTNAME=$(arping -c 1 -I "$ETH" "$IP" | grep "hostname" | awk '{print $4}' | sed 's/.$//') # arping失败则用nmap兜底 if [[ -z "$HOSTNAME" ]]; then HOSTNAME=$(nmap -sP "$IP" | grep -m 1 "Nmap scan report for" | awk '{print $5}') fi # 最终兜底用IP作为主机名 HOSTNAME=${HOSTNAME:-$IP} # 生成邮件内容 BODY=$(printf "检测到未知设备的DHCP分配,详情如下:\n\n时间:%s\nIP地址:%s\nMAC地址:%s\n主机名:%s\n网卡接口:%s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$IP" "$MAC" "$HOSTNAME" "$ETH") # 发送邮件并输出日志 python3 /root/sendmail.py -s "$SUBJECT" -a "$BODY" echo "$BODY" # 更新最后检查时间 date +%s > "$LAST_CHECK_FILE" fi } # 脚本启动时,先处理日志中最近20行(防止重启后遗漏未处理的记录) tail -n 20 "$NEW_DHCP_FILE" | while read -r line; do process_log_line "$line" done # 实时跟踪新日志行 tail -F "$NEW_DHCP_FILE" | while read -r line; do process_log_line "$line" done
额外建议
- 给脚本添加守护进程管理(比如OPNSense的
rc.d脚本),确保脚本意外退出后自动重启。 - 定期清理
/tmp/last_dhcp_check文件,或在脚本启动时检查文件是否过期。 tail -F会自动处理日志轮转,无需额外配置。
内容的提问来源于stack exchange,提问作者Robson
相关产品推荐
相关产品推荐

