from typing import List, Dict, Any, Optional from sqlalchemy.orm import Session from datetime import datetime from concurrent.futures import ThreadPoolExecutor, as_completed import subprocess import socket import ipaddress import re from app.models.network import ScanTask, Network, IPAddress from app.models.network import TaskStatus, TaskType, IPStatus from app.core.config import settings logger = __import__('logging').getLogger(__name__) class EnhancedScanService: """增强扫描服务 - 包含ARP、DNS解析、MAC厂商识别""" # MAC OUI 厂商映射(常用厂商) 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 get_mac_vendor(mac_address: str) -> Optional[str]: """根据MAC地址OUI识别厂商""" if not mac_address: return None # 标准化MAC格式为 AA:BB:CC:DD:EE:FF mac = mac_address.upper().replace('-', ':') if len(mac) < 8: return None # 提取前3字节 OUI oui = mac[:8] # 精确匹配 if oui in EnhancedScanService.OUI_MAP: return EnhancedScanService.OUI_MAP[oui] # 尝试模糊匹配(前2字节) oui_short = mac[:5] for key, vendor in EnhancedScanService.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解析获取主机名""" try: socket.setdefaulttimeout(timeout) hostname, _, _ = socket.gethostbyaddr(ip_address) return hostname except (socket.herror, socket.timeout, socket.gaierror): return None @staticmethod def arp_scan_single(ip_address: str, timeout: int = 2) -> Optional[str]: """ 对单个IP执行ARP扫描获取MAC地址 返回: MAC地址 或 None """ # 方法1: 使用 arp-scan 命令(比 scapy 更可靠,不依赖 raw socket) try: result = subprocess.run( ['arp-scan', '-I', 'auto', ip_address, '-q'], 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: continue parts = line.split('\t') if len(parts) >= 2: mac = parts[1].strip() if mac and mac != '': return mac.upper() except Exception: pass # 方法2: 尝试读取系统 ARP 缓存 return EnhancedScanService._get_arp_from_cache(ip_address) @staticmethod def _get_arp_from_cache(ip_address: str) -> Optional[str]: """从系统ARP缓存读取MAC地址""" try: # 读取 /proc/net/arp (Linux) with open('/proc/net/arp', 'r') as f: for line in f.readlines()[1:]: # 跳过表头 parts = line.split() if len(parts) >= 4 and parts[0] == ip_address: mac = parts[3].upper() if mac != '00:00:00:00:00:00': return mac except Exception: pass # 尝试 arp -a 命令 try: result = subprocess.run( ['arp', '-a', ip_address], capture_output=True, text=True, 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: return match.group().upper() except Exception: pass return None @staticmethod def comprehensive_scan(ip_address: str, enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, timeout: int = 2) -> Dict[str, Any]: """ 综合扫描单个IP 返回: { 'ip_address': str, 'status': 'online'|'offline', 'mac_address': str|None, 'hostname': str|None, 'vendor': str|None, 'response_time': float|None } """ result = { 'ip_address': ip_address, 'status': 'offline', 'mac_address': None, 'hostname': None, 'vendor': None, 'success': False } # 1. Ping 扫描 if enable_ping: ping_result = EnhancedScanService.ping_single_ip(ip_address, timeout) result['status'] = ping_result.get('status', 'offline') result['success'] = ping_result.get('success', False) result['response_time'] = ping_result.get('response_time') # 2. ARP 扫描 (即使ping不通也尝试,有些设备禁ping) if enable_arp: mac = EnhancedScanService.arp_scan_single(ip_address, timeout) if mac: result['mac_address'] = mac 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) if hostname: result['hostname'] = hostname return result @staticmethod def ping_single_ip(ip_address: str, timeout: int = 2) -> Dict[str, Any]: """Ping 单个IP""" start_time = datetime.now() try: result = subprocess.run( ['ping', '-c', '1', '-W', str(timeout), ip_address], capture_output=True, text=True ) is_alive = result.returncode == 0 response_time = (datetime.now() - start_time).total_seconds() * 1000 return { 'ip_address': ip_address, 'status': 'online' if is_alive else 'offline', 'response_time': round(response_time, 2), 'success': is_alive } except Exception as e: return { 'ip_address': ip_address, 'status': 'error', 'error': str(e), 'success': False } @staticmethod def scan_network(cidr: str, enable_ping: bool = True, enable_arp: bool = True, enable_dns: bool = True, timeout: int = 2, max_concurrent: int = 50) -> List[Dict[str, Any]]: """ 扫描整个网段(并发) """ network = ipaddress.ip_network(cidr, strict=False) ips = [str(ip) for ip in network.hosts()] if not ips: return [] # 并发执行;并发数随网段大小自适应 worker_count = min(max_concurrent, len(ips)) results: List[Dict[str, Any]] = [] with ThreadPoolExecutor(max_workers=worker_count) as executor: future_to_ip = { executor.submit( EnhancedScanService.comprehensive_scan, ip, enable_ping=enable_ping, enable_arp=enable_arp, enable_dns=enable_dns, timeout=timeout ): ip for ip in ips } for future in as_completed(future_to_ip): try: results.append(future.result()) except Exception as e: ip = future_to_ip[future] results.append({ 'ip_address': ip, 'status': 'offline', 'mac_address': None, 'hostname': None, 'vendor': None, 'success': False, 'error': str(e) }) return results @staticmethod def update_ip_from_scan_result(db: Session, scan_result: Dict[str, Any]): """根据扫描结果更新IP信息""" ip_address = scan_result.get('ip_address') if not ip_address: return db_ip = db.query(IPAddress).filter(IPAddress.ip_address == ip_address).first() # 如果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): network = net break except Exception: pass if not network: logger.warning(f"无法为扫描到的IP {ip_address} 找到所属网段,跳过创建") return db_ip = IPAddress(ip_address=ip_address, network_id=network.id) db.add(db_ip) db.commit() db.refresh(db_ip) # 更新状态 status = scan_result.get('status', 'offline') if status == 'online': db_ip.status = IPStatus.ONLINE elif status == 'offline' and db_ip.status == IPStatus.ONLINE: db_ip.status = IPStatus.OFFLINE # 更新 MAC 地址 mac = scan_result.get('mac_address') if mac: db_ip.mac_address = mac # 更新主机名 hostname = scan_result.get('hostname') if hostname: db_ip.hostname = hostname # 更新厂商信息 vendor = scan_result.get('vendor') if vendor: db_ip.vendor = vendor # 更新最后发现时间 if scan_result.get('success'): db_ip.last_seen = datetime.utcnow() return db_ip @staticmethod def bulk_update_ips_from_scan(db: Session, scan_results: List[Dict[str, Any]]): """批量更新IP信息""" online_count = 0 mac_found_count = 0 hostname_found_count = 0 created_count = 0 for result in scan_results: ip = EnhancedScanService.update_ip_from_scan_result(db, result) if ip: if result.get('success'): online_count += 1 if result.get('mac_address'): mac_found_count += 1 if result.get('hostname'): hostname_found_count += 1 db.commit() return { 'online_count': online_count, 'mac_found_count': mac_found_count, 'hostname_found_count': hostname_found_count, 'total_scanned': len(scan_results) }