diff --git a/core/scanner.py b/core/scanner.py index d871680..42aae38 100644 --- a/core/scanner.py +++ b/core/scanner.py @@ -9,57 +9,42 @@ from concurrent.futures import ThreadPoolExecutor, as_completed _NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0 -def _scan_with_scapy(network): - """Scan using scapy (requires root/sudo, and Npcap on Windows).""" - from scapy.all import ARP, Ether, srp - - arp = ARP(pdst=str(network)) - ether = Ether(dst="ff:ff:ff:ff:ff:ff") - - packet = ether / arp - - result = srp(packet, timeout=3, verbose=0)[0] - - devices = [] - - for sent, received in result: - devices.append({ - "ip": received.psrc, - "mac": received.hwsrc - }) - - return devices - - def _ping_one(ip, is_win): """Ping a single IP to populate ARP table.""" try: if is_win: subprocess.run( - ["ping", "-n", "1", "-w", "600", str(ip)], + ["ping", "-n", "1", "-w", "300", str(ip)], # 300ms (was 600ms) stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=3, + timeout=2, creationflags=_NO_WINDOW ) + elif sys.platform == "darwin": + subprocess.run( + ["ping", "-c", "1", "-W", "500", str(ip)], # 500ms — macOS: -W unit là ms + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + timeout=2 + ) else: subprocess.run( - ["ping", "-c", "1", "-W", "1", str(ip)], + ["ping", "-c", "1", "-W", "1", str(ip)], # 1s — Linux: -W unit là giây stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - timeout=3 + timeout=2 ) except Exception: pass def _ping_sweep(network, progress_cb=None): - """Ping all IPs in network concurrently to populate ARP table. - Calls progress_cb(done, total) after each ping completes if provided. + """Ping tất cả host trong network đồng thời để điền ARP cache. + Gọi progress_cb(done, total) sau mỗi ping nếu được cung cấp. """ net = ipaddress.ip_network(network, strict=False) - # Only ping sweep for /24 or smaller to avoid flooding + # Chỉ ping sweep cho /24 hoặc nhỏ hơn if net.num_addresses > 256: return @@ -74,151 +59,97 @@ def _ping_sweep(network, progress_cb=None): if progress_cb: progress_cb(done_count[0], total) - with ThreadPoolExecutor(max_workers=100) as executor: + # Tất cả host cùng lúc — loại bỏ overhead batching (was max_workers=100) + with ThreadPoolExecutor(max_workers=len(hosts)) as executor: futures = [executor.submit(_ping_and_track, ip) for ip in hosts] for f in as_completed(futures): pass -def _scan_with_arp_table(network): - """Fallback: read ARP table using system 'arp -a' (no root needed). - Supports both macOS and Windows output formats. - """ - # Ping sweep first to populate ARP table with active devices - _ping_sweep(network) - - # Brief pause to let the OS finalize ARP cache entries - time.sleep(1) - - try: - output = subprocess.check_output( - ["arp", "-a"], text=True, creationflags=_NO_WINDOW - ) - except Exception: - return [] - - devices = [] - net = ipaddress.ip_network(network, strict=False) - - if sys.platform == "win32": - # Windows format: - # 192.168.4.1 cc-2d-21-a5-85-b0 dynamic - pattern = re.compile( - r"(\d+\.\d+\.\d+\.\d+)\s+" - r"([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-" - r"[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})\s+" - r"(dynamic|static)", - re.IGNORECASE - ) - - for line in output.splitlines(): - m = pattern.search(line) - if m: - ip_str = m.group(1) - # Convert Windows MAC format (cc-2d-21-a5-85-b0) to standard (cc:2d:21:a5:85:b0) - mac = m.group(2).replace("-", ":") - if mac.upper() != "FF:FF:FF:FF:FF:FF": - try: - if ipaddress.ip_address(ip_str) in net: - devices.append({"ip": ip_str, "mac": mac}) - except ValueError: - pass - else: - # macOS/Linux format: - # ? (192.168.1.1) at aa:bb:cc:dd:ee:ff on en0 - pattern = re.compile( - r"\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)" - ) - - for line in output.splitlines(): - m = pattern.search(line) - if m: - ip_str, mac = m.group(1), m.group(2) - if mac.lower() != "(incomplete)" and mac != "ff:ff:ff:ff:ff:ff": - try: - if ipaddress.ip_address(ip_str) in net: - devices.append({"ip": ip_str, "mac": mac}) - except ValueError: - pass - - return devices - - def scan_network(network, progress_cb=None, stage_cb=None): - """Scan network: ping sweep first, then merge scapy ARP + arp table.""" - # Phase 1: Ping sweep — wake up devices and populate ARP cache + """Scan network: ping sweep → ARP table + Scapy song song.""" + # Phase 1: Ping sweep if stage_cb: stage_cb("ping") _ping_sweep(network, progress_cb) - time.sleep(1) + time.sleep(0.3) # Giảm từ 1s xuống 0.3s - # Collect results from both methods and merge by IP - seen = {} # ip -> device dict - - # Phase 2: ARP table (populated by ping sweep above) + # Phase 2 + 3: ARP table và Scapy chạy song song if stage_cb: stage_cb("arp") - try: - output = subprocess.check_output( - ["arp", "-a"], text=True, creationflags=_NO_WINDOW - ) - net = ipaddress.ip_network(network, strict=False) - if sys.platform == "win32": - pattern = re.compile( - r"(\d+\.\d+\.\d+\.\d+)\s+" - r"([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-" - r"[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})\s+" - r"(dynamic|static)", - re.IGNORECASE - ) - for line in output.splitlines(): - m = pattern.search(line) - if m: - ip_str = m.group(1) - mac = m.group(2).replace("-", ":") - if mac.upper() != "FF:FF:FF:FF:FF:FF": - try: - if ipaddress.ip_address(ip_str) in net: - seen[ip_str] = {"ip": ip_str, "mac": mac} - except ValueError: - pass - else: - pattern = re.compile( - r"\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)" - ) - for line in output.splitlines(): - m = pattern.search(line) - if m: - ip_str, mac = m.group(1), m.group(2) - if mac.lower() not in ("(incomplete)", "ff:ff:ff:ff:ff:ff"): - try: - if ipaddress.ip_address(ip_str) in net: - seen[ip_str] = {"ip": ip_str, "mac": mac} - except ValueError: - pass - except Exception: - pass - # Phase 3: scapy ARP scan (if Npcap available) — fills in any gaps - if stage_cb: - stage_cb("scapy") - try: - import io, os - _stderr = sys.stderr - sys.stderr = io.StringIO() + def _collect_arp(): + result = {} try: - from scapy.all import ARP, Ether, srp - finally: - sys.stderr = _stderr + output = subprocess.check_output( + ["arp", "-a"], text=True, creationflags=_NO_WINDOW + ) + net = ipaddress.ip_network(network, strict=False) + if sys.platform == "win32": + pattern = re.compile( + r"(\d+\.\d+\.\d+\.\d+)\s+" + r"([0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-" + r"[0-9a-fA-F]{2}-[0-9a-fA-F]{2}-[0-9a-fA-F]{2})\s+" + r"(dynamic|static)", + re.IGNORECASE + ) + for line in output.splitlines(): + m = pattern.search(line) + if m: + ip_str = m.group(1) + mac = m.group(2).replace("-", ":") + if mac.upper() != "FF:FF:FF:FF:FF:FF": + try: + if ipaddress.ip_address(ip_str) in net: + result[ip_str] = {"ip": ip_str, "mac": mac} + except ValueError: + pass + else: + pattern = re.compile( + r"\((\d+\.\d+\.\d+\.\d+)\)\s+at\s+([0-9a-fA-F:]+)" + ) + for line in output.splitlines(): + m = pattern.search(line) + if m: + ip_str, mac = m.group(1), m.group(2) + if mac.lower() not in ("(incomplete)", "ff:ff:ff:ff:ff:ff"): + try: + if ipaddress.ip_address(ip_str) in net: + result[ip_str] = {"ip": ip_str, "mac": mac} + except ValueError: + pass + except Exception: + pass + return result - arp = ARP(pdst=str(network)) - ether = Ether(dst="ff:ff:ff:ff:ff:ff") - result = srp(ether / arp, timeout=2, verbose=0)[0] - for sent, received in result: - ip = received.psrc + def _collect_scapy(): + result = {} + try: + import io + _stderr = sys.stderr + sys.stderr = io.StringIO() + try: + from scapy.all import ARP, Ether, srp + finally: + sys.stderr = _stderr + + arp = ARP(pdst=str(network)) + ether = Ether(dst="ff:ff:ff:ff:ff:ff") + raw = srp(ether / arp, timeout=1, verbose=0)[0] # Giảm từ 2s xuống 1s + for _, received in raw: + result[received.psrc] = {"ip": received.psrc, "mac": received.hwsrc} + except Exception: + pass + return result + + # Chạy ARP read và Scapy đồng thời + with ThreadPoolExecutor(max_workers=2) as executor: + f_arp = executor.submit(_collect_arp) + f_scapy = executor.submit(_collect_scapy) + seen = f_arp.result() + # Scapy bổ sung những IP chưa có trong ARP table + for ip, dev in f_scapy.result().items(): if ip not in seen: - seen[ip] = {"ip": ip, "mac": received.hwsrc} - except Exception: - pass + seen[ip] = dev - return sorted(seen.values(), key=lambda d: ipaddress.ip_address(d["ip"])) \ No newline at end of file + return sorted(seen.values(), key=lambda d: ipaddress.ip_address(d["ip"])) diff --git a/version.txt b/version.txt index 45a1b3f..781dcb0 100644 --- a/version.txt +++ b/version.txt @@ -1 +1 @@ -1.1.2 +1.1.3