第342篇:DHCP 服务故障与排障实战案例

关键词

DHCP、DHCP Snooping、DHCP Relay、IP地址冲突、地址池耗尽、Giaddr、Option 82、中继


一、案例背景

1.1 故障现象

某企业园区网 DHCP 故障:

时间:周一早上 8:30(上班高峰期) 影响:新接入用户无法获取 IP 地址

现象:

新员工接入网络: └─ 网卡显示 "正在获取 IP 地址" └─ 等待 30 秒后显示 "受限" └─ 手动配置静态 IP 可以上网 已有用户: └─ 在线用户不受影响 └─ 续约正常 DHCP 服务器状态: └─ CPU 正常(15%) └─ 地址池显示有空闲地址 └─ 日志无错误

1.2 网络拓扑

园区 DHCP 组网:

          ┌──────────────────────┐
          │  DHCP Server         │
          │  (Windows/Linux)     │
          └──────────┬───────────┘
                     │
          ┌──────────┴───────────┐
          │  核心交换机 Core      │
          │  (DHCP Relay)        │
          └──────────┬───────────┘
                     │
          ┌──────────┴───────────┐
          │  汇聚交换机 Agg       │
          └──────────┬───────────┘
                     │
          ┌──────────┴───────────┐
          │  接入交换机 Access    │
          │  (DHCP Snooping)     │
          └──────────┬───────────┘
                     │
                ┌────┴────┐
                │  PC     │
                └─────────┘

二、故障排查过程

2.1 初步排查

排查路线:

  第一步:检查 DHCP 服务器
  ┌──────────────────────────────────────────┐
  │  # 查看 DHCP 地址池状态                   │
  │  > netsh dhcp server scope 10.1.1.0      │
  │    show clients                          │
  │  > 地址池:10.1.1.0/24                   │
  │  > 已分配:220/254 地址                  │
  │  > 空闲:34 个地址                       │
  │  > 排除:10 个地址                       │
  │                                           │
  │  结论:地址池正常,还有空闲地址          │
  └──────────────────────────────────────────┘

  第二步:抓包分析(在接入交换机端口抓包)
  ┌──────────────────────────────────────────┐
  │  客户端 → DHCP Discover(广播)           │
  │  DHCP Discover                           │
  │    src: 0.0.0.0                          │
  │    dst: 255.255.255.255                  │
  │    chaddr: xx:xx:xx:xx:xx:xx             │
  │                                           │
  │  ✅ 客户端成功发出 Discover              │
  │  ❌ 客户端未收到 Offer                   │
  │  ❌ 未看到中继转发到服务器的包            │
  │                                           │
  │  问题:DHCP Discover 未到达服务器        │
  └──────────────────────────────────────────┘

2.2 深入排查

第三步:检查 DHCP Snooping

  ┌──────────────────────────────────────────┐
  │  # 查看 DHCP Snooping 配置               │
  │  display dhcp snooping configuration     │
  │                                           │
  │  DHCP Snooping 配置:                     │
  │  ┌──────────────────────────────────────┐ │
  │  │  dhcp snooping enable                │ │
  │  │  dhcp snooping trusted interface     │ │
  │  │   GigabitEthernet0/0/24  (上联口)    │ │
  │  │  dhcp snooping trusted interface     │ │
  │  │   GigabitEthernet0/0/23  (上联口)    │ │
  │  └──────────────────────────────────────┘ │
  │                                           │
  │  # 查看 DHCP Snooping 绑定表             │
  │  display dhcp snooping user-table all     │
  │  ┌──────────────────────────────────────┐ │
  │  │  MAC           IP          VLAN Port │ │
  │  │  xx:xx:xx:01  10.1.1.10  100  0/0/1 │ │
  │  │  xx:xx:xx:02  10.1.1.11  100  0/0/2 │ │
  │  │  ...                                 │ │
  │  │  绑定表有 220 条记录                  │ │
  │  └──────────────────────────────────────┘ │
  │                                           │
  │  初步看配置正常...                        │
  └──────────────────────────────────────────┘

  第四步:检查 DHCP Relay(核心交换机发现关键问题)
  ┌──────────────────────────────────────────┐
  │  # 查看 DHCP Relay 配置                  │
  │  display dhcp relay configuration        │
  │                                           │
  │  interface Vlanif100                     │
  │   dhcp relay server 10.1.0.5             │
  │                                           │
  │  # 查看 DHCP Relay 统计                  │
  │  display dhcp relay statistics            │
  │  ┌──────────────────────────────────────┐ │
  │  │  Received from client: 1500 packets │ │
  │  │  Sent to server:     0 packets      │ │ ← 问题!
  │  │  Received from server: 0 packets    │ │
  │  │  Sent to client:      0 packets     │ │
  │  └──────────────────────────────────────┘ │
  │                                           │
  │  发现:Relay 收到客户端包,但没发给服务器  │
  │  根因:Relay 配置的服务器 IP 无法到达     │
  └──────────────────────────────────────────┘

2.3 根因定位

最终根因分析:

  问题链路追踪:
  ┌──────────────────────────────────────────┐
  │  Core 到 DHCP Server 路径:              │
  │                                           │
  │  Core ── 防火墙 ── DHCP Server           │
  │  10.1.0.1    10.1.0.2    10.1.0.5       │
  │                                           │
  │  # 在 Core 上测试连通性                   │
  │  ping 10.1.0.5                           │
  │  └─ 不通!                                │
  │                                           │
  │  # 检查路由表                             │
  │  display ip routing-table 10.1.0.5       │
  │  └─ 路由指向防火墙 10.1.0.2              │
  │                                           │
  │  # 检查防火墙策略                         │
  │  └─ 防火墙管理员说昨天做了策略变更        │
  │  └─ 添加了新的安全域隔离规则             │
  │  └─ DHCP 中继流量(UDP 67/68)被阻断     │
  │                                           │
  │  根因:防火墙策略变更导致 DHCP Relay     │
  │       流量被阻断                          │
  └──────────────────────────────────────────┘

三、解决方案

3.1 紧急恢复

紧急恢复措施:

  方案一:临时放通防火墙策略
  ┌──────────────────────────────────────────┐
  │  # 防火墙放通 DHCP 中继流量              │
  │  security-policy                         │
  │   rule name permit_dhcp_relay            │
  │    source-zone trust                     │
  │    destination-zone dmz                  │
  │    source-address 10.1.0.1 24           │
  │    destination-address 10.1.0.5 32      │
  │    service udp_dhcp                      │  # UDP 67/68
  │    action permit                         │
  │  #                                        │
  │  # 效果:客户端立即获取到 IP              │
  └──────────────────────────────────────────┘

  方案二:配置 DHCP 多跳 Relay(跨三层)
  ┌──────────────────────────────────────────┐
  │  # 如果无法放通防火墙(安全要求高)       │
  │  # 可在接入交换机直接配置 Relay          │
  │                                           │
  │  interface Vlanif100                     │
  │   dhcp relay server 10.1.0.5             │
  │   dhcp relay source-interface LoopBack0  │
  │  # 确保接入交换机到服务器的路由可达      │
  └──────────────────────────────────────────┘

3.2 长期优化

长期优化方案:

  方案一:DHCP Server 双机热备
  ┌──────────────────────────────────────────┐
  │  DHCP Server-1 (Active)                  │
  │  DHCP Server-2 (Standby)                 │
  │                                           │
  │  配置:                                    │
  │  └─ 地址池自动同步                       │
  │  └─ 健康检查:每 30 秒检查              │
  │  └─ 故障切换:< 1 分钟                  │
  │                                           │
  │  Relay 配置双服务器:                      │
  │  dhcp relay server 10.1.0.5             │
  │  dhcp relay server 10.1.0.6             │
  └──────────────────────────────────────────┘

  方案二:DHCP Snooping 安全加固
  ┌──────────────────────────────────────────┐
  │  # 限制端口 DHCP 请求速率                 │
  │  interface GigabitEthernet0/0/1          │
  │   dhcp snooping check dhcp-rate enable    │
  │   dhcp snooping dhcp-rate 10              │
  │  # 每端口最多 10 包/秒                   │
  │                                           │
  │  # 启用 Option 82 插入                   │
  │  dhcp snooping option82 enable            │
  │  dhcp snooping option82 insert            │
  │  # 便于定位 DHCP 请求来源                │
  │                                           │
  │  # 启用 MAC 地址校验                     │
  │  dhcp snooping check mac-address          │
  └──────────────────────────────────────────┘

四、自动化监控脚本

#!/usr/bin/env python3
"""
DHCP 服务健康监控脚本
检查 DHCP 服务器可达性、地址池水位、Relay 统计
"""

import subprocess
import re
import time
import json
from datetime import datetime


class DHCPHealthMonitor:
    """DHCP 健康监控器"""

    def __init__(self, relay_ip, server_ips, pool_subnets):
        self.relay_ip = relay_ip
        self.server_ips = server_ips
        self.pool_subnets = pool_subnets
        self.status = {}

    def check_server_reachability(self, ip):
        """检查 DHCP 服务器可达性"""
        try:
            result = subprocess.run(
                ["ping", "-n", "2", ip],
                capture_output=True,
                text=True,
                timeout=10
            )
            return result.returncode == 0
        except subprocess.TimeoutExpired:
            return False

    def check_relay_statistics(self):
        """检查 DHCP Relay 统计(模拟)"""
        # 实际场景通过 SSH/Netconf 采集
        stats = {
            "received_from_client": 1500,
            "sent_to_server": 1500,  # 正常应非零
            "received_from_server": 1500,
            "sent_to_client": 1500,
            "discard_packets": 0,
            "error_packets": 0
        }
        return stats

    def check_pool_utilization(self, subnet):
        """检查地址池利用率(模拟)"""
        total = 254
        used = 220
        reserved = 10
        free = total - used - reserved

        return {
            "subnet": subnet,
            "total": total,
            "used": used,
            "free": free,
            "utilization_pct": round(used / total * 100, 1)
        }

    def run_health_check(self):
        """执行完整健康检查"""
        timestamp = datetime.now().isoformat()
        results = {
            "timestamp": timestamp,
            "server_status": {},
            "relay_status": {},
            "pool_status": [],
            "overall_status": "HEALTHY"
        }

        # 检查服务器
        all_servers_ok = True
        for server_ip in self.server_ips:
            reachable = self.check_server_reachability(server_ip)
            results["server_status"][server_ip] = {
                "reachable": reachable
            }
            if not reachable:
                all_servers_ok = False

        # 检查 Relay
        relay_stats = self.check_relay_statistics()
        relay_ok = (
            relay_stats["sent_to_server"] > 0
            and relay_stats["sent_to_client"] > 0
        )
        results["relay_status"] = {
            "stats": relay_stats,
            "healthy": relay_ok
        }

        # 检查地址池
        pool_warning = False
        for subnet in self.pool_subnets:
            pool = self.check_pool_utilization(subnet)
            results["pool_status"].append(pool)
            if pool["utilization_pct"] > 90:
                pool_warning = True

        # 综合状态
        if not all_servers_ok:
            results["overall_status"] = "CRITICAL"
            results["alert"] = "DHCP 服务器不可达!"
        elif not relay_ok:
            results["overall_status"] = "CRITICAL"
            results["alert"] = "DHCP Relay 异常!"
        elif pool_warning:
            results["overall_status"] = "WARNING"
            results["alert"] = "地址池使用率超过 90%!"
        else:
            results["overall_status"] = "HEALTHY"

        self.status = results
        return results

    def print_report(self):
        """打印检查报告"""
        if not self.status:
            self.run_health_check()

        status = self.status
        print(f"\nDHCP 健康检查报告")
        print(f"时间: {status['timestamp']}")
        print(f"状态: {status['overall_status']}")
        print("=" * 50)

        if "alert" in status:
            print(f"⚠ 告警: {status['alert']}\n")

        print("DHCP 服务器状态:")
        for ip, info in status["server_status"].items():
            symbol = "✅" if info["reachable"] else "❌"
            print(f"  {symbol} {ip}: {'可达' if info['reachable'] else '不可达'}")

        print("\nDHCP Relay 状态:")
        relay = status["relay_status"]
        print(f"  {'✅' if relay['healthy'] else '❌'} 转发正常")
        print(f"  收到客户端: {relay['stats']['received_from_client']}")
        print(f"  发往服务器: {relay['stats']['sent_to_server']}")
        print(f"  收到服务器: {relay['stats']['received_from_server']}")

        print("\n地址池状态:")
        for pool in status["pool_status"]:
            bar = "█" * int(pool["utilization_pct"] / 5)
            print(f"  {pool['subnet']}: [{bar:<20}] {pool['utilization_pct']}%")
            print(f"    已用: {pool['used']}, 空闲: {pool['free']}, 总: {pool['total']}")


def main():
    """主函数"""
    monitor = DHCPHealthMonitor(
        relay_ip="10.1.0.1",
        server_ips=["10.1.0.5", "10.1.0.6"],
        pool_subnets=["10.1.1.0/24", "10.1.2.0/24", "10.1.3.0/24"]
    )

    # 运行检查
    monitor.run_health_check()
    monitor.print_report()

    # 持续监控示例
    print("\n\n持续监控模式(每 60 秒检查一次)...")
    print("按 Ctrl+C 退出")
    try:
        while True:
            monitor.run_health_check()
            if monitor.status["overall_status"] != "HEALTHY":
                print(f"\n[{datetime.now().isoformat()}] "
                      f"告警: {monitor.status.get('alert', '未知')}")
            time.sleep(60)
    except KeyboardInterrupt:
        print("\n监控已停止")


if __name__ == "__main__":
    main()

五、DHCP 排障常见场景

常见 DHCP 故障场景速查表:

场景 可能原因 排查方向 Discover 无响应 Relay 故障/防火墙 检查 Relay 统计 获取到 169.254.x.x DHCP Server 不可达 检查服务器连通 IP 地址冲突 Snooping 表过期 清除绑定表 获取慢(>10秒) Option 82 处理慢 优化 Relay 部分用户获取不到 地址池耗尽 增加地址池 续约失败 租期太短 调整租期 收到非法 DHCP Server Snooping 未配置 启用 Snooping VLAN 间获取不到 Relay 未配置 VLANIF 配置 Relay


六、总结

DHCP 故障排查要点:

  1. 分层排查
     └─ 客户端 → 接入交换机 → 汇聚 → Core → 服务器
     └─ 逐段确认 DHCP 报文是否正常转发

  2. 关键检查点
     └─ DHCP Snooping 绑定表(检查是否满)
     └─ DHCP Relay 统计(确认转发正常)
     └─ 防火墙策略(UDP 67/68 是否放通)
     └─ 地址池利用率(避免耗尽)

  3. 预防措施
     └─ 部署双 DHCP Server 冗余
     └─ 配置 DHCP Snooping 安全功能
     └─ 定期检查地址池水位
     └─ 变更防火墙策略前评估 DHCP 影响
     └─ 监控 Relay 统计,发现异常及时处理

下篇预告:第343篇《防火墙策略冲突与安全域隔离故障案例》——通过防火墙策略冲突案例,掌握安全域划分和策略优化的实战技能。


下篇预告:第343篇《防火墙策略冲突与安全域隔离故障案例》——通过防火墙策略冲突案例,掌握安全域划分和策略优化的实战技能。