fix bug scan ip

This commit is contained in:
2026-03-10 17:54:56 +07:00
parent 466dadf1c9
commit f5ee209726
3 changed files with 79 additions and 154 deletions

View File

@@ -1,7 +1,5 @@
import subprocess import subprocess
import re
import sys import sys
import time
import ipaddress import ipaddress
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
@@ -10,146 +8,72 @@ _NO_WINDOW = subprocess.CREATE_NO_WINDOW if sys.platform == "win32" else 0
def _ping_one(ip, is_win): def _ping_one(ip, is_win):
"""Ping a single IP to populate ARP table.""" """Ping a single IP. Returns True if host responds."""
try: try:
if is_win: if is_win:
subprocess.run( r = subprocess.run(
["ping", "-n", "1", "-w", "300", str(ip)], # 300ms (was 600ms) ["ping", "-n", "1", "-w", "300", str(ip)],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=2, timeout=2,
creationflags=_NO_WINDOW creationflags=_NO_WINDOW
) )
elif sys.platform == "darwin": elif sys.platform == "darwin":
subprocess.run( r = subprocess.run(
["ping", "-c", "1", "-W", "500", str(ip)], # 500ms — macOS: -W unit là ms ["ping", "-c", "1", "-W", "300", str(ip)],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=2 timeout=2
) )
else: else:
subprocess.run( r = subprocess.run(
["ping", "-c", "1", "-W", "1", str(ip)], # 1s — Linux: -W unit là giây ["ping", "-c", "1", "-W", "1", str(ip)],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
timeout=2 timeout=2
) )
return r.returncode == 0
except Exception: except Exception:
pass return False
def _ping_sweep(network, progress_cb=None): def _ping_sweep(network, progress_cb=None):
"""Ping tất cả host trong network đồng thời để điền ARP cache. """Ping tất cả host trong network đồng thời.
Gọi progress_cb(done, total) sau mỗi ping nếu được cung cấp. Trả về list IP đã phản hồi. Gọi progress_cb(done, total) sau mỗi ping.
""" """
net = ipaddress.ip_network(network, strict=False) net = ipaddress.ip_network(network, strict=False)
# Chỉ ping sweep cho /24 hoặc nhỏ hơn
if net.num_addresses > 256: if net.num_addresses > 256:
return return []
is_win = sys.platform == "win32" is_win = sys.platform == "win32"
hosts = list(net.hosts()) hosts = list(net.hosts())
total = len(hosts) total = len(hosts)
done_count = [0] done_count = [0]
alive = []
def _ping_and_track(ip): def _ping_and_track(ip):
_ping_one(ip, is_win) ok = _ping_one(ip, is_win)
done_count[0] += 1 done_count[0] += 1
if progress_cb: if progress_cb:
progress_cb(done_count[0], total) progress_cb(done_count[0], total)
return (str(ip), ok)
# 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: with ThreadPoolExecutor(max_workers=len(hosts)) as executor:
futures = [executor.submit(_ping_and_track, ip) for ip in hosts] futures = [executor.submit(_ping_and_track, ip) for ip in hosts]
for f in as_completed(futures): for f in as_completed(futures):
pass ip_str, ok = f.result()
if ok:
alive.append(ip_str)
return alive
def scan_network(network, progress_cb=None, stage_cb=None): def scan_network(network, progress_cb=None, stage_cb=None):
"""Scan network: ping sweep → ARP table + Scapy song song.""" """Scan network: chỉ dùng ping để xác định thiết bị online."""
# Phase 1: Ping sweep
if stage_cb: if stage_cb:
stage_cb("ping") stage_cb("ping")
_ping_sweep(network, progress_cb) alive_ips = _ping_sweep(network, progress_cb)
time.sleep(0.3) # Giảm từ 1s xuống 0.3s
# Phase 2 + 3: ARP table và Scapy chạy song song results = [{"ip": ip_str, "mac": "N/A"} for ip_str in alive_ips]
if stage_cb: return sorted(results, key=lambda d: ipaddress.ip_address(d["ip"]))
stage_cb("arp")
def _collect_arp():
result = {}
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:
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
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] = dev
return sorted(seen.values(), key=lambda d: ipaddress.ip_address(d["ip"]))

View File

@@ -2,37 +2,40 @@
## 1. Tổng quan ## 1. Tổng quan
Thành phần quét IP trong `scanner.py` dò tìm và liệt kê tất cả thiết bị đang hoạt động trên một dải mạng (ví dụ: `192.168.1.0/24`), trả về danh sách chứa **IP****MAC Address** của từng thiết bị. Thành phần quét IP trong `scanner.py` dò tìm và liệt kê tất cả thiết bị **đang thực sự online** trên một dải mạng (ví dụ: `192.168.1.0/24`), trả về danh sách chứa **IP** của từng thiết bị.
Để đảm bảo tỷ lệ phát hiện cao trên mọi hệ điều hành (Windows, macOS, Linux), scanner kết hợp 3 giai đoạn: Scanner chỉ sử dụng **ICMP Ping** — thiết bị phải phản hồi ping thì mới được liệt kê. Cơ chế này đảm bảo kết quả chính xác, tránh hiện tượng hiển thị thiết bị đã ngắt kết nối do ARP cache cũ trên router/OS.
1. **Ping Sweep** — đánh thức thiết bị, điền ARP cache > **Lý do loại bỏ ARP và Scapy:** Bảng ARP của hệ điều hành và router giữ cache entry trong nhiều phút, khiến thiết bị đã rút vẫn xuất hiện trong kết quả scan. Ping trực tiếp là phương pháp duy nhất xác nhận thiết bị thực sự đang hoạt động tại thời điểm scan.
2. **Đọc bảng ARP hệ điều hành** (`arp -a`) — không cần quyền Admin
3. **Scapy ARP broadcast** — bổ sung thiết bị chặn ICMP
Bước 2 và 3 chạy **song song** để giảm tổng thời gian scan.
--- ---
## 2. Luồng hoạt động chính (Hàm `scan_network`) ## 2. Luồng hoạt động chính (Hàm `scan_network`)
```
scan_network(network)
├── stage_cb("ping") ← Thông báo UI bắt đầu quét
├── _ping_sweep(network) ← Ping đồng thời toàn bộ host
│ │
│ ├── _ping_one(ip) × N ← Mỗi host 1 thread
│ │
│ └── return [alive IPs]
└── return [{"ip": ..., "mac": "N/A"}, ...] ← Sorted by IP
```
**Bước 1 — Ping Sweep** **Bước 1 — Ping Sweep**
- Gọi `_ping_sweep(network)`: gửi ICMP Echo Request đồng thời tới **toàn bộ host** trong dải mạng. - Gọi `_ping_sweep(network)`: gửi ICMP Echo Request đồng thời tới **toàn bộ host** trong dải mạng.
- Mỗi thiết bị phản hồi sẽ khiến hệ điều hành **tự ghi MAC vào ARP Cache**. - Chỉ các IP có `returncode == 0` (phản hồi thành công) mới được đưa vào kết quả.
- Sau khi sweep xong, chờ `0.3s` để OS kịp finalize ARP cache (giảm từ 1s trước đây).
**Bước 2 + 3 — ARP Table & Scapy (song song)** **Bước 2 — Trả về kết quả**
- Hai hàm `_collect_arp()``_collect_scapy()` được submit vào `ThreadPoolExecutor(max_workers=2)` và chạy đồng thời:
- `_collect_arp()`: đọc `arp -a`, parse regex lấy IP + MAC.
- `_collect_scapy()`: gửi ARP broadcast, nhận phản hồi trực tiếp từ thiết bị.
- Kết quả merge theo IP: ARP table làm nền, Scapy bổ sung IP còn thiếu.
**Bước 4 — Trả về kết quả**
- Danh sách sort tăng dần theo IP: - Danh sách sort tăng dần theo IP:
`[{"ip": "192.168.1.2", "mac": "aa:bb:cc:dd:ee:ff"}, ...]` `[{"ip": "192.168.1.2", "mac": "N/A"}, ...]`
- Trường `mac` luôn là `"N/A"` — giữ nguyên cấu trúc dict để tương thích với UI và các module khác.
--- ---
@@ -40,48 +43,45 @@ Bước 2 và 3 chạy **song song** để giảm tổng thời gian scan.
### `_ping_one(ip, is_win)` ### `_ping_one(ip, is_win)`
Ping một IP đơn lẻ với timeout tối ưu theo nền tảng: Ping một IP đơn lẻ, trả về `True` nếu host phản hồi, `False` nếu không.
Timeout tối ưu theo nền tảng:
| OS | Lệnh | Timeout wait | Timeout process | | OS | Lệnh | Timeout wait | Timeout process |
| ------- | ------------------ | ----------------- | --------------- | | ------- | ------------------ | ------------ | --------------- |
| Windows | `ping -n 1 -w 300` | 300ms | 2s | | Windows | `ping -n 1 -w 300` | 300ms | 2s |
| macOS | `ping -c 1 -W 500` | 500ms (đơn vị ms) | 2s | | macOS | `ping -c 1 -W 300` | 300ms | 2s |
| Linux | `ping -c 1 -W 1` | 1s (đơn vị giây) | 2s | | Linux | `ping -c 1 -W 1` | 1s | 2s |
> macOS và Linux dùng cùng flag `-W` nhưng đơn vị khác nhau — được xử lý tách biệt theo `sys.platform`. > macOS và Linux dùng cùng flag `-W` nhưng đơn vị khác nhau (macOS: ms, Linux: giây) — được xử lý tách biệt theo `sys.platform`.
Windows sử dụng `CREATE_NO_WINDOW` flag để tránh mở cửa sổ console cho mỗi subprocess.
### `_ping_sweep(network, progress_cb)` ### `_ping_sweep(network, progress_cb)`
- Tạo `ThreadPoolExecutor(max_workers=len(hosts))`**toàn bộ host ping đồng thời**, không batching. - Tạo `ThreadPoolExecutor(max_workers=len(hosts))`**toàn bộ host ping đồng thời**, không batching.
- Giới hạn an toàn: chỉ chạy với subnet `/24` trở xuống (`num_addresses <= 256`). - Giới hạn an toàn: chỉ chạy với subnet `/24` trở xuống (`num_addresses <= 256`).
- Trả về danh sách IP (string) đã phản hồi thành công.
- Gọi `progress_cb(done, total)` sau mỗi ping để UI cập nhật thanh tiến độ. - Gọi `progress_cb(done, total)` sau mỗi ping để UI cập nhật thanh tiến độ.
### `_collect_arp()` (nội bộ trong `scan_network`) ### `scan_network(network, progress_cb, stage_cb)`
Đọc và parse output `arp -a`, hỗ trợ đa nền tảng: - Entry point chính.
- `stage_cb("ping")` thông báo UI giai đoạn hiện tại.
- **Windows:** Regex nhận dạng dạng `cc-2d-21-a5-85-b0 dynamic`, chuẩn hóa MAC sang `cc:2d:21:a5:85:b0`. - Trả về `list[dict]` với format `{"ip": str, "mac": "N/A"}`, sorted theo IP tăng dần.
- **macOS/Linux:** Regex nhận dạng dạng `(192.168.1.1) at aa:bb:cc:dd:ee:ff`, bỏ qua entry `(incomplete)`.
### `_collect_scapy()` (nội bộ trong `scan_network`)
- Gửi ARP `Who-has` broadcast (`Ether/ARP` qua `srp`) với **timeout 1s** (giảm từ 2s).
- Stderr bị redirect tạm thời khi import scapy để tránh spam log ra console.
- Tự động bỏ qua nếu scapy không khả dụng (không có Npcap / không có quyền root).
--- ---
## 4. So sánh hiệu năng (trước và sau tối ưu) ## 4. Hiệu năng
| Thay đổi | Trước | Sau | | Thông số | Giá trị |
| --------------------------- | --------------------- | ------------------------------------ | | --------------------------- | ------------------------------------ |
| Ping workers | 100 (batching ~3 đợt) | `len(hosts)` (~254, tất cả cùng lúc) | | Ping workers | `len(hosts)` (~254, tất cả cùng lúc) |
| Ping timeout — Windows | 600ms | 300ms | | Ping timeout — Windows | 300ms |
| Ping timeout — macOS | 1ms (sai đơn vị) | 500ms | | Ping timeout — macOS | 300ms |
| Sleep sau ping sweep | 1.0s | 0.3s | | Ping timeout — Linux | 1s |
| ARP + Scapy | Tuần tự | **Song song** | | Process timeout | 2s |
| Scapy timeout | 2s | 1s | | **Tổng thời gian scan /24** | **~12s** |
| **Tổng thời gian scan /24** | ~57s | **~1.52s** |
--- ---
@@ -89,13 +89,14 @@ Ping một IP đơn lẻ với timeout tối ưu theo nền tảng:
**Ưu điểm:** **Ưu điểm:**
- Tỷ lệ phát hiện thiết bị cao nhờ kết hợp 3 lớp. - **Kết quả chính xác** — chỉ thiết bị thực sự online mới xuất hiện, không bị ảnh hưởng bởi ARP cache cũ.
- Không cần quyền Admin/Root — ping sweep + ARP table vẫn tìm được ~90% thiết bị. - Không cần quyền Admin/Root.
- Tương thích đa nền tảng (Windows/macOS/Linux) qua xử lý riêng từng OS. - Không phụ thuộc thư viện bên ngoài (không cần Scapy, Npcap).
- ARP table và Scapy chạy song song → không cộng dồn thời gian chờ. - Tương thích đa nền tảng (Windows/macOS/Linux).
- Nhanh (~12s cho /24) nhờ ping toàn bộ host đồng thời.
**Nhược điểm:** **Nhược điểm:**
- Vẫn cần `sleep(0.3s)` để OS kịp ghi ARP cache sau ping sweep. - Không có thông tin MAC address.
- Thiết bị tắt hoàn toàn cả ICMP lẫn ARP sẽ không bị phát hiện. - Thiết bị chặn ICMP (tắt ping) sẽ không bị phát hiện.
- Spawn ~254 process `ping` đồng thời trên Windows có overhead cao hơn Unix do Windows tạo process chậm hơn. - Spawn ~254 process `ping` đồng thời trên Windows có overhead cao hơn Unix.

View File

@@ -1 +1 @@
1.2.1 1.2.2