Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 45 additions & 42 deletions network_scanner/gui.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,7 @@
import threading
import os
import asyncio
from concurrent.futures import ThreadPoolExecutor, as_completed
from .scanner import check_npcap, load_mac_prefixes, get_ip_range, scan_network, get_mac_vendor, logger
from .scanner import check_npcap, load_mac_prefixes, get_ip_range, scan_network, logger
from .block import (block_via_arp_poison, block_via_arp_flood, block_via_arp_tornado,
block_via_mac_flood, block_via_icmp_unreachable, block_via_tcp_syn_flood,
block_via_dns_amplification)
Expand Down Expand Up @@ -246,7 +245,8 @@ def update_treeview_columns(self):
def refresh_treeview_data(self):
for device in self.current_devices:
row_data = self.build_row_data(device)
self.tree.insert("", tk.END, values=row_data)
item_id = self.tree.insert("", tk.END, values=row_data)
device["tree_id"] = item_id

def build_row_data(self, device):
row = [device.get("ip", "Unknown"), device.get("mac", "Unknown")]
Expand All @@ -260,6 +260,39 @@ def build_row_data(self, device):
row.append(",".join(map(str, ports)) if ports else "None")
return tuple(row)

def handle_device_found(self, device):
"""Handle a newly discovered device during scanning.

Insert the device into the Treeview immediately so the user sees
progress in real time. Additional details such as the OS guess and
open ports are gathered in a background thread and the row is updated
once the information is ready.
"""

def process_device():
try:
device["os_guess"] = fingerprint_os(device["ip"])
except Exception:
device["os_guess"] = "Unknown"
try:
device["open_ports"] = probe_open_ports(device["ip"])
except Exception:
device["open_ports"] = []

def update_row():
self.tree.item(device["tree_id"], values=self.build_row_data(device))

self.master.after(0, update_row)

def add_row():
row_data = self.build_row_data(device)
item_id = self.tree.insert("", tk.END, values=row_data)
device["tree_id"] = item_id
self.current_devices.append(device)
threading.Thread(target=process_device, daemon=True).start()

self.master.after(0, add_row)

# ----------------------- Scanning -----------------------
def run_scan(self):
self.update_status("Starting network scan...")
Expand All @@ -283,58 +316,28 @@ def thread_scan(self):
ip_range = get_ip_range()
self.network = ip_range

# Step 4: Scan the network asynchronously.
devices = asyncio.run(scan_network(ip_range, self.mac_prefixes))

# Initialize vendor and port info.
for device in devices:
device["vendor"] = "Not Fetched"
device["open_ports"] = []
device["os_guess"] = "Unknown"
# Step 4: Scan the network asynchronously and report devices as found.
asyncio.run(
scan_network(
ip_range,
self.mac_prefixes,
progress_callback=self.handle_device_found,
)
)

# Step 5: Get vendor info if enabled.
if self.show_vendor_var.get():
for device in devices:
device["vendor"] = get_mac_vendor(device.get("mac", ""), self.mac_prefixes)

# Step 6: Get OS guess if enabled using advanced fingerprinting.
if self.show_os_var.get():
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_device = {
executor.submit(fingerprint_os, device["ip"]): device for device in devices
}
for future in as_completed(future_to_device):
dev = future_to_device[future]
try:
dev["os_guess"] = future.result()
except Exception:
dev["os_guess"] = "Unknown"
# Step 7: Scan open ports if enabled.
if self.show_port_var.get():
with ThreadPoolExecutor(max_workers=10) as executor:
future_to_device = {
executor.submit(probe_open_ports, device["ip"]): device for device in devices
}
for future in as_completed(future_to_device):
dev = future_to_device[future]
try:
dev["open_ports"] = future.result()
except Exception:
dev["open_ports"] = []
self.current_devices = devices
self.master.after(0, self.post_scan_update)
except Exception as e:
logger.error(f"An error occurred during scanning: {e}")
self.master.after(0, lambda: self.scan_button.config(state="normal"))
self.master.after(0, lambda: self.update_status("Ready"))

def post_scan_update(self):
self.refresh_treeview_data()
self.scan_button.config(state="normal")
if not self.current_devices:
messagebox.showinfo("Scan Complete", "No devices found on the network.")
else:
messagebox.showinfo("Scan Complete", f"Found {len(self.current_devices)} device(s).")
self.update_status("Ready")

# ----------------------- Blocking -----------------------
def update_block_parameters(self, *args):
Expand Down
62 changes: 38 additions & 24 deletions network_scanner/scanner.py
Original file line number Diff line number Diff line change
Expand Up @@ -163,8 +163,12 @@ def get_ip_range():
logger.error(f"Failed to determine local IP range: {e}")
raise

async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2):
"""Asynchronously scan a subnet for active devices."""
async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2, progress_callback=None):
"""Asynchronously scan a subnet for active devices.

If ``progress_callback`` is provided it will be called for each device
as soon as it responds, allowing callers to react in real time.
"""
results = []

if not check_npcap():
Expand All @@ -174,20 +178,7 @@ async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2):
broadcast = Ether(dst="ff:ff:ff:ff:ff:ff")
packet = broadcast / arp_req

try:
# Use AsyncSniffer without a timeout so we control when to stop it.
sniffer = AsyncSniffer(filter="arp and arp[6:2] == 2")
sniffer.start()
await asyncio.to_thread(sendp, packet, verbose=False)
await asyncio.sleep(timeout)
# sniffer.stop() returns the captured packets.
answered = sniffer.stop()
logger.info(f"Found {len(answered)} devices in subnet {subnet}")
except Exception as e:
logger.error(f"Error scanning subnet {subnet}: {e}")
return results

for pkt in answered:
def handle_packet(pkt):
if ARP in pkt and pkt[ARP].op == 2:
device = {
'ip': pkt[ARP].psrc,
Expand All @@ -196,10 +187,31 @@ async def scan_subnet(subnet, mac_prefixes, timeout=3, retry=2):
'device_name': get_device_name(pkt[ARP].psrc)
}
results.append(device)
if progress_callback:
progress_callback(device)

try:
# Use AsyncSniffer with a packet handler to stream results immediately.
sniffer = AsyncSniffer(
filter="arp and arp[6:2] == 2", prn=handle_packet, store=False
)
sniffer.start()
await asyncio.to_thread(sendp, packet, verbose=False)
await asyncio.sleep(timeout)
sniffer.stop()
logger.info(f"Found {len(results)} devices in subnet {subnet}")
except Exception as e:
logger.error(f"Error scanning subnet {subnet}: {e}")

return results

async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24):
"""Asynchronously scan a network by scanning subnets concurrently."""
async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24,
progress_callback=None):
"""Asynchronously scan a network by scanning subnets concurrently.

``progress_callback`` is passed to each subnet scan and invoked for every
device as soon as it is discovered.
"""
devices = []
try:
subnets = list(ip_range.subnets(new_prefix=subnet_prefix))
Expand All @@ -211,16 +223,18 @@ async def scan_network(ip_range, mac_prefixes, max_workers=10, subnet_prefix=24)

async def worker(subnet):
async with semaphore:
return await scan_subnet(subnet, mac_prefixes)
return await scan_subnet(
subnet, mac_prefixes, progress_callback=progress_callback
)

tasks = [asyncio.create_task(worker(subnet)) for subnet in subnets]
results = await asyncio.gather(*tasks, return_exceptions=True)

for result in results:
if isinstance(result, Exception):
logger.error(f"Error processing subnet: {result}")
else:
for coro in asyncio.as_completed(tasks):
try:
result = await coro
devices.extend(result)
except Exception as e:
logger.error(f"Error processing subnet: {e}")

logger.info(f"Scan completed. Found {len(devices)} devices")
except Exception as e:
Expand Down
Loading