You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

优化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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 07:56:05