update scan ip
This commit is contained in:
155
core/scanner.py
155
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
|
_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):
|
def _ping_one(ip, is_win):
|
||||||
"""Ping a single IP to populate ARP table."""
|
"""Ping a single IP to populate ARP table."""
|
||||||
try:
|
try:
|
||||||
if is_win:
|
if is_win:
|
||||||
subprocess.run(
|
subprocess.run(
|
||||||
["ping", "-n", "1", "-w", "600", str(ip)],
|
["ping", "-n", "1", "-w", "300", str(ip)], # 300ms (was 600ms)
|
||||||
stdout=subprocess.DEVNULL,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
timeout=3,
|
timeout=2,
|
||||||
creationflags=_NO_WINDOW
|
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:
|
else:
|
||||||
subprocess.run(
|
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,
|
stdout=subprocess.DEVNULL,
|
||||||
stderr=subprocess.DEVNULL,
|
stderr=subprocess.DEVNULL,
|
||||||
timeout=3
|
timeout=2
|
||||||
)
|
)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _ping_sweep(network, progress_cb=None):
|
def _ping_sweep(network, progress_cb=None):
|
||||||
"""Ping all IPs in network concurrently to populate ARP table.
|
"""Ping tất cả host trong network đồng thời để điền ARP cache.
|
||||||
Calls progress_cb(done, total) after each ping completes if provided.
|
Gọi progress_cb(done, total) sau mỗi ping nếu được cung cấp.
|
||||||
"""
|
"""
|
||||||
net = ipaddress.ip_network(network, strict=False)
|
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:
|
if net.num_addresses > 256:
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -74,90 +59,27 @@ def _ping_sweep(network, progress_cb=None):
|
|||||||
if progress_cb:
|
if progress_cb:
|
||||||
progress_cb(done_count[0], total)
|
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]
|
futures = [executor.submit(_ping_and_track, ip) for ip in hosts]
|
||||||
for f in as_completed(futures):
|
for f in as_completed(futures):
|
||||||
pass
|
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):
|
def scan_network(network, progress_cb=None, stage_cb=None):
|
||||||
"""Scan network: ping sweep first, then merge scapy ARP + arp table."""
|
"""Scan network: ping sweep → ARP table + Scapy song song."""
|
||||||
# Phase 1: Ping sweep — wake up devices and populate ARP cache
|
# Phase 1: Ping sweep
|
||||||
if stage_cb:
|
if stage_cb:
|
||||||
stage_cb("ping")
|
stage_cb("ping")
|
||||||
_ping_sweep(network, progress_cb)
|
_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
|
# Phase 2 + 3: ARP table và Scapy chạy song song
|
||||||
seen = {} # ip -> device dict
|
|
||||||
|
|
||||||
# Phase 2: ARP table (populated by ping sweep above)
|
|
||||||
if stage_cb:
|
if stage_cb:
|
||||||
stage_cb("arp")
|
stage_cb("arp")
|
||||||
|
|
||||||
|
def _collect_arp():
|
||||||
|
result = {}
|
||||||
try:
|
try:
|
||||||
output = subprocess.check_output(
|
output = subprocess.check_output(
|
||||||
["arp", "-a"], text=True, creationflags=_NO_WINDOW
|
["arp", "-a"], text=True, creationflags=_NO_WINDOW
|
||||||
@@ -179,7 +101,7 @@ def scan_network(network, progress_cb=None, stage_cb=None):
|
|||||||
if mac.upper() != "FF:FF:FF:FF:FF:FF":
|
if mac.upper() != "FF:FF:FF:FF:FF:FF":
|
||||||
try:
|
try:
|
||||||
if ipaddress.ip_address(ip_str) in net:
|
if ipaddress.ip_address(ip_str) in net:
|
||||||
seen[ip_str] = {"ip": ip_str, "mac": mac}
|
result[ip_str] = {"ip": ip_str, "mac": mac}
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
@@ -193,17 +115,17 @@ def scan_network(network, progress_cb=None, stage_cb=None):
|
|||||||
if mac.lower() not in ("(incomplete)", "ff:ff:ff:ff:ff:ff"):
|
if mac.lower() not in ("(incomplete)", "ff:ff:ff:ff:ff:ff"):
|
||||||
try:
|
try:
|
||||||
if ipaddress.ip_address(ip_str) in net:
|
if ipaddress.ip_address(ip_str) in net:
|
||||||
seen[ip_str] = {"ip": ip_str, "mac": mac}
|
result[ip_str] = {"ip": ip_str, "mac": mac}
|
||||||
except ValueError:
|
except ValueError:
|
||||||
pass
|
pass
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
return result
|
||||||
|
|
||||||
# Phase 3: scapy ARP scan (if Npcap available) — fills in any gaps
|
def _collect_scapy():
|
||||||
if stage_cb:
|
result = {}
|
||||||
stage_cb("scapy")
|
|
||||||
try:
|
try:
|
||||||
import io, os
|
import io
|
||||||
_stderr = sys.stderr
|
_stderr = sys.stderr
|
||||||
sys.stderr = io.StringIO()
|
sys.stderr = io.StringIO()
|
||||||
try:
|
try:
|
||||||
@@ -213,12 +135,21 @@ def scan_network(network, progress_cb=None, stage_cb=None):
|
|||||||
|
|
||||||
arp = ARP(pdst=str(network))
|
arp = ARP(pdst=str(network))
|
||||||
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
ether = Ether(dst="ff:ff:ff:ff:ff:ff")
|
||||||
result = srp(ether / arp, timeout=2, verbose=0)[0]
|
raw = srp(ether / arp, timeout=1, verbose=0)[0] # Giảm từ 2s xuống 1s
|
||||||
for sent, received in result:
|
for _, received in raw:
|
||||||
ip = received.psrc
|
result[received.psrc] = {"ip": received.psrc, "mac": received.hwsrc}
|
||||||
if ip not in seen:
|
|
||||||
seen[ip] = {"ip": ip, "mac": received.hwsrc}
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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"]))
|
return sorted(seen.values(), key=lambda d: ipaddress.ip_address(d["ip"]))
|
||||||
@@ -1 +1 @@
|
|||||||
1.1.2
|
1.1.3
|
||||||
|
|||||||
Reference in New Issue
Block a user