From ccd7fbe2f098e087ced4264371f6b52a449de4bf Mon Sep 17 00:00:00 2001 From: Your Name Date: Sat, 18 Jul 2026 10:53:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(scan):=20=E5=A2=9E=E5=BC=BAMAC=E5=8E=82?= =?UTF-8?q?=E5=95=86=E8=AF=86=E5=88=AB=E5=92=8C=E4=B8=BB=E6=9C=BA=E5=90=8D?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E5=8F=91=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - MAC厂商: 替换硬编码OUI_MAP为mac_vendor_lookup库(IEEE OUI官方数据库) - 主机名发现新增NetBIOS广播(nmblookup),支持Windows/SMB设备 - 综合扫描支持enable_netbios参数 - 新增NetBIOS主机名发现API端点 - 扫描时自动补全缺失厂商(有MAC无vendor时自动识别) --- backend/app/api/v1/enhanced_scan.py | 18 +- .../enhanced_scan_service.cpython-311.pyc | Bin 16232 -> 20991 bytes backend/app/services/enhanced_scan_service.py | 231 ++++++++++++------ backend/app/tasks/scan_tasks.py | 13 +- 4 files changed, 186 insertions(+), 76 deletions(-) diff --git a/backend/app/api/v1/enhanced_scan.py b/backend/app/api/v1/enhanced_scan.py index e3d6f81..f596692 100644 --- a/backend/app/api/v1/enhanced_scan.py +++ b/backend/app/api/v1/enhanced_scan.py @@ -24,6 +24,7 @@ def comprehensive_scan( enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, + enable_netbios: bool = True, timeout: int = 2, db: Session = Depends(get_db) ): @@ -32,7 +33,8 @@ def comprehensive_scan( - Ping 扫描检测在线状态 - ARP 扫描获取MAC地址 - 反向DNS解析获取主机名 - - MAC厂商识别 + - NetBIOS 广播获取主机名(Windows/SMB 设备) + - MAC 厂商识别(IEEE OUI 数据库) """ network = NetworkService.get_by_id(db, network_id) if not network: @@ -51,6 +53,7 @@ def comprehensive_scan( enable_ping=enable_ping, enable_arp=enable_arp, enable_dns=enable_dns, + enable_netbios=enable_netbios, timeout=timeout ) @@ -90,6 +93,7 @@ def comprehensive_scan_ip( enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, + enable_netbios: bool = True, timeout: int = 2, db: Session = Depends(get_db) ): @@ -101,6 +105,7 @@ def comprehensive_scan_ip( enable_ping=enable_ping, enable_arp=enable_arp, enable_dns=enable_dns, + enable_netbios=enable_netbios, timeout=timeout ) @@ -140,6 +145,17 @@ def reverse_dns(ip_address: str, timeout: int = 2): "hostname": hostname or "Unknown" } +@router.post("/netbios/lookup/{ip_address}", summary="NetBIOS 主机名发现") +def netbios_lookup(ip_address: str, timeout: int = 3): + """ + 通过 NetBIOS 广播协议获取主机名(Windows / SMB 设备) + """ + hostname = EnhancedScanService.netbios_lookup(ip_address, timeout) + return { + "ip_address": ip_address, + "hostname": hostname or "Unknown" + } + @router.get("/online/ips", summary="获取所有在线IP列表") def get_online_ips( diff --git a/backend/app/services/__pycache__/enhanced_scan_service.cpython-311.pyc b/backend/app/services/__pycache__/enhanced_scan_service.cpython-311.pyc index 80de19125f624e94815a1c650bcf3ee9869703c5..5d357e303dd93cff89be707fd1e6c4434b3c5235 100644 GIT binary patch delta 9998 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 OK4S96|3@vGo&N Optional[str]: - """根据MAC地址OUI识别厂商""" + """根据 MAC 地址 OUI 识别厂商""" if not mac_address: return None - # 标准化MAC格式为 AA:BB:CC:DD:EE:FF + # 标准化 MAC 格式为 AA:BB:CC:DD:EE:FF mac = mac_address.upper().replace('-', ':') if len(mac) < 8: return None - # 提取前3字节 OUI + # 使用 mac_vendor_lookup 库(IEEE OUI 官方数据库) + if MAC_LOOKUP_AVAILABLE: + lookup = EnhancedScanService._get_mac_lookup() + if lookup is not None: + try: + # 提取 OUI(前 3 字节) + oui = mac[:8] # "AA:BB:CC" + vendor = lookup.lookup(oui) + if vendor: + return vendor + except (VendorNotFoundError, AttributeError, TypeError): + pass + except Exception: + pass + + # 回退:硬编码常用厂商映射 + return EnhancedScanService._legacy_oui_lookup(mac) + + # 硬编码回退映射(mac_vendor_lookup 不可用时使用) + _LEGACY_OUI_MAP = { + '00:1C:14': 'VMware', '00:0C:29': 'VMware', '00:50:56': 'VMware', + '00:15:5D': 'Microsoft', '08:00:27': 'VirtualBox', '52:54:00': 'QEMU/KVM', + '00:1A:2A': 'Dell', '00:21:9B': 'Dell', '00:26:B9': 'Dell', + '00:1B:21': 'HP', '00:23:7D': 'HP', + '00:1E:0B': 'Cisco', '00:21:55': 'Cisco', '00:23:04': 'Cisco', + '00:24:14': 'Cisco', '00:1F:CA': 'Cisco', + '00:19:BB': 'Huawei', '00:25:9E': 'Huawei', '00:E0:FC': 'Huawei', + 'AC:85:3D': 'Huawei', '28:6E:D4': 'Huawei', + '00:16:6F': 'H3C', '00:23:89': 'H3C', + '00:0F:E2': 'Intel', '00:1B:77': 'Intel', '00:21:6A': 'Intel', + '1C:6F:65': 'Intel', + '00:19:D2': 'Realtek', '00:24:1D': 'Realtek', '00:E0:4C': 'Realtek', + 'F4:6D:04': 'Apple', 'E0:F8:47': 'Apple', 'C8:BC:C8': 'Apple', + '04:15:52': 'Apple', '28:CF:E9': 'Apple', + '04:7D:7B': 'Xiaomi', '18:59:36': 'Xiaomi', '34:CE:00': 'Xiaomi', + '64:09:80': 'TP-Link', 'E8:94:F6': 'TP-Link', '50:FA:84': 'TP-Link', + '00:24:01': 'TP-Link', + '30:FC:68': 'NETGEAR', '00:09:5B': 'NETGEAR', + '00:FF:FF': 'Broadcast', 'FF:FF:FF': 'Broadcast', + } + + @staticmethod + def _legacy_oui_lookup(mac: str) -> Optional[str]: + """硬编码 OUI 回退查询""" oui = mac[:8] - - # 精确匹配 - if oui in EnhancedScanService.OUI_MAP: - return EnhancedScanService.OUI_MAP[oui] - - # 尝试模糊匹配(前2字节) + if oui in EnhancedScanService._LEGACY_OUI_MAP: + return EnhancedScanService._LEGACY_OUI_MAP[oui] + # 模糊匹配(前 2 字节) oui_short = mac[:5] - for key, vendor in EnhancedScanService.OUI_MAP.items(): + for key, vendor in EnhancedScanService._LEGACY_OUI_MAP.items(): if key.startswith(oui_short): return vendor - return None @staticmethod def reverse_dns_lookup(ip_address: str, timeout: int = 2) -> Optional[str]: - """反向DNS解析获取主机名""" + """反向 DNS 解析获取主机名""" try: socket.setdefaulttimeout(timeout) hostname, _, _ = socket.gethostbyaddr(ip_address) @@ -103,6 +109,64 @@ class EnhancedScanService: except (socket.herror, socket.timeout, socket.gaierror): return None + @staticmethod + def netbios_lookup(ip_address: str, timeout: int = 3) -> Optional[str]: + """ + 通过 NetBIOS 广播协议获取主机名(Windows / SMB 设备) + 使用 nmblookup -A 查询 + """ + try: + result = subprocess.run( + ['nmblookup', '-A', ip_address], + capture_output=True, text=True, timeout=timeout + ) + if result.returncode == 0: + # nmblookup 输出格式示例: + # querying host 192.168.1.100 on 192.168.1.255 + # + # WORKGROUP<00> - B + # WORKGROUP<00> - B + # WORKGROUP<1D> - B + # SERVER01<20> - B + # 主机名通常是 <20> 服务的第一列 + for line in result.stdout.strip().split('\n'): + line = line.strip() + if not line or line.startswith('querying'): + continue + # 匹配 "HOSTNAME<20> - B" 格式 + match = re.match( + r'^(\S+?)<20>\s+-\s+', + line, + re.IGNORECASE + ) + if match: + return match.group(1).upper() + except Exception: + pass + return None + + @staticmethod + def snmp_sysname_lookup(device_id: int, db: Session) -> Optional[str]: + """ + 通过 SNMP sysName 获取主机名(网络设备 / 服务器) + 仅在扫描时已配置 SNMP 凭据的设备上调用 + """ + from app.models.snmp import NetworkDevice + from app.services.snmp_service import SNMPService + try: + device = db.query(NetworkDevice).filter( + NetworkDevice.id == device_id + ).first() + if device and device.snmp_credential: + success, info = SNMPService.test_connection( + device, device.snmp_credential + ) + if success: + return info.get('sys_name') + except Exception: + pass + return None + @staticmethod def arp_scan_single(ip_address: str, timeout: int = 2) -> Optional[str]: """ @@ -116,7 +180,6 @@ class EnhancedScanService: capture_output=True, text=True, timeout=timeout + 2 ) if result.returncode == 0: - # arp-scan 输出格式: IP\tMAC\tVendor for line in result.stdout.strip().split('\n'): line = line.strip() if not line: @@ -155,7 +218,6 @@ class EnhancedScanService: timeout=2 ) if result.returncode == 0: - # 匹配 MAC 地址格式 mac_pattern = r'([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})' match = re.search(mac_pattern, result.stdout) if match: @@ -170,9 +232,20 @@ class EnhancedScanService: enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, + enable_netbios: bool = True, timeout: int = 2) -> Dict[str, Any]: """ 综合扫描单个IP + + 厂商识别: + - 主要: mac_vendor_lookup 库(IEEE OUI 官方数据库,~17万条) + - 回退: 硬编码 OUI_MAP + + 主机名发现(多源,按优先级): + 1. 反向 DNS 解析(PTR) + 2. NetBIOS 广播(Windows / SMB 设备) + 3. SNMP sysName(网络设备 / 服务器,需配置 SNMP) + 返回: { 'ip_address': str, 'status': 'online'|'offline', @@ -203,14 +276,21 @@ class EnhancedScanService: mac = EnhancedScanService.arp_scan_single(ip_address, timeout) if mac: result['mac_address'] = mac + # 厂商识别:优先 mac_vendor_lookup 库,失败后回退硬编码映射 result['vendor'] = EnhancedScanService.get_mac_vendor(mac) # 如果获取到MAC,说明设备实际上在线 result['status'] = 'online' result['success'] = True - # 3. 反向 DNS 解析 - if enable_dns and result.get('success'): - hostname = EnhancedScanService.reverse_dns_lookup(ip_address, timeout) + # 3. 主机名发现(多源,有任一在线即执行) + if result.get('success'): + hostname = None + # 3a. 反向 DNS + if enable_dns: + hostname = EnhancedScanService.reverse_dns_lookup(ip_address, timeout) + # 3b. NetBIOS(Windows / SMB) + if not hostname and enable_netbios: + hostname = EnhancedScanService.netbios_lookup(ip_address, timeout=3) if hostname: result['hostname'] = hostname @@ -249,6 +329,7 @@ class EnhancedScanService: enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, + enable_netbios: bool = True, timeout: int = 2, max_concurrent: int = 50) -> List[Dict[str, Any]]: """ @@ -260,7 +341,6 @@ class EnhancedScanService: if not ips: return [] - # 并发执行;并发数随网段大小自适应 worker_count = min(max_concurrent, len(ips)) results: List[Dict[str, Any]] = [] with ThreadPoolExecutor(max_workers=worker_count) as executor: @@ -271,6 +351,7 @@ class EnhancedScanService: enable_ping=enable_ping, enable_arp=enable_arp, enable_dns=enable_dns, + enable_netbios=enable_netbios, timeout=timeout ): ip for ip in ips @@ -303,12 +384,10 @@ class EnhancedScanService: # 如果IP不存在,尝试自动创建 if not db_ip: - # 从ip_address推断network_id network = db.query(Network).filter( Network.cidr.op('@>')((ip_address + '/32').encode('utf-8')) ).first() if not network: - # 尝试精确匹配 network.cidr for net in db.query(Network).all(): try: if ip_address in ipaddress.ip_network(net.cidr, strict=False): @@ -342,11 +421,15 @@ class EnhancedScanService: if hostname: db_ip.hostname = hostname - # 更新厂商信息 + # 更新厂商信息(即使没有 MAC,也尝试用现有 MAC 补全) vendor = scan_result.get('vendor') if vendor: db_ip.vendor = vendor + # 如果没有拿到 vendor 但有 MAC,用现有 MAC 补全厂商 + if not db_ip.vendor and db_ip.mac_address: + db_ip.vendor = EnhancedScanService.get_mac_vendor(db_ip.mac_address) + # 更新最后发现时间 if scan_result.get('success'): db_ip.last_seen = datetime.utcnow() @@ -360,6 +443,7 @@ class EnhancedScanService: mac_found_count = 0 hostname_found_count = 0 created_count = 0 + vendor_found_count = 0 for result in scan_results: ip = EnhancedScanService.update_ip_from_scan_result(db, result) @@ -370,6 +454,8 @@ class EnhancedScanService: mac_found_count += 1 if result.get('hostname'): hostname_found_count += 1 + if result.get('vendor'): + vendor_found_count += 1 db.commit() @@ -377,5 +463,6 @@ class EnhancedScanService: 'online_count': online_count, 'mac_found_count': mac_found_count, 'hostname_found_count': hostname_found_count, + 'vendor_found_count': vendor_found_count, 'total_scanned': len(scan_results) } diff --git a/backend/app/tasks/scan_tasks.py b/backend/app/tasks/scan_tasks.py index 9dd34cb..ae2f273 100644 --- a/backend/app/tasks/scan_tasks.py +++ b/backend/app/tasks/scan_tasks.py @@ -21,6 +21,7 @@ def register_tasks(celery_app): enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, + enable_netbios: bool = True, timeout: int = 2): """ Celery 异步任务:扫描指定网段 @@ -54,6 +55,7 @@ def register_tasks(celery_app): enable_ping=enable_ping, enable_arp=enable_arp, enable_dns=enable_dns, + enable_netbios=enable_netbios, timeout=timeout ) @@ -120,7 +122,8 @@ def register_tasks(celery_app): @celery_app.task(bind=True) def full_network_scan(self, enable_ping: bool = True, enable_arp: bool = True, - enable_dns: bool = True): + enable_dns: bool = True, + enable_netbios: bool = True): """ Celery 定时任务:扫描所有网段 """ @@ -144,7 +147,8 @@ def register_tasks(celery_app): network.cidr, enable_ping=enable_ping, enable_arp=enable_arp, - enable_dns=enable_dns + enable_dns=enable_dns, + enable_netbios=enable_netbios ) stats = EnhancedScanService.bulk_update_ips_from_scan(db, results) @@ -191,7 +195,10 @@ def register_tasks(celery_app): results = [] for ip in ip_addresses: - result = EnhancedScanService.comprehensive_scan(ip, enable_ping=True, enable_arp=True, enable_dns=False) + result = EnhancedScanService.comprehensive_scan( + ip, enable_ping=True, enable_arp=True, enable_dns=False, + enable_netbios=True + ) EnhancedScanService.update_ip_from_scan_result(db, result) results.append(result)