v2.0 streaming redesign: concurrent scan+spray, RDP connect button, live stats fix, terminal output
This commit is contained in:
141
scanner.py
Normal file
141
scanner.py
Normal file
@@ -0,0 +1,141 @@
|
||||
"""
|
||||
FastRDP-NG: Blazing-fast async RDP scanner.
|
||||
Connects to thousands of IPs concurrently to find live RDP hosts.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import time
|
||||
from typing import Set, Tuple, List
|
||||
|
||||
logger = logging.getLogger("FastRDP-NG")
|
||||
|
||||
# RDP ports to scan
|
||||
RDP_PORTS = [3389, 3390, 3391]
|
||||
|
||||
# First 3 bytes of an RDP Negotiation Response (T.125)
|
||||
# Actual RDP servers respond with 0x03 (TPKT version 3)
|
||||
RDP_BANNER_SIG = b'\x03\x00'
|
||||
|
||||
|
||||
async def check_rdp_port(
|
||||
ip: str,
|
||||
port: int,
|
||||
timeout: float = 2.0
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Rapid async TCP connect check.
|
||||
Returns (ip, port, is_open).
|
||||
No banner reading - pure connection speed.
|
||||
"""
|
||||
try:
|
||||
_, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(ip, port),
|
||||
timeout=timeout
|
||||
)
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
return (ip, port, True)
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError,
|
||||
OSError, ConnectionError):
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def check_rdp_with_banner(
|
||||
ip: str,
|
||||
port: int,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_timeout: float = 1.0
|
||||
) -> Tuple[str, int, bool]:
|
||||
"""
|
||||
Connect + read initial bytes to confirm it's actually RDP.
|
||||
Slower but more accurate - catches non-RDP services on 3389.
|
||||
"""
|
||||
try:
|
||||
reader, writer = await asyncio.wait_for(
|
||||
asyncio.open_connection(ip, port),
|
||||
timeout=connect_timeout
|
||||
)
|
||||
try:
|
||||
data = await asyncio.wait_for(
|
||||
reader.read(4),
|
||||
timeout=banner_timeout
|
||||
)
|
||||
is_rdp = data[:2] == RDP_BANNER_SIG # TPKT header
|
||||
return (ip, port, is_rdp)
|
||||
except (asyncio.TimeoutError, ConnectionError, OSError):
|
||||
# Connected but no banner = port open but not RDP
|
||||
return (ip, port, False)
|
||||
finally:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except (asyncio.TimeoutError, ConnectionRefusedError,
|
||||
OSError, ConnectionError):
|
||||
return (ip, port, False)
|
||||
|
||||
|
||||
async def scan_ips(
|
||||
ips: List[str],
|
||||
ports: List[int] = None, # type: ignore
|
||||
max_concurrent: int = 5000,
|
||||
connect_timeout: float = 2.0,
|
||||
banner_check: bool = False,
|
||||
progress_callback=None,
|
||||
live_callback=None
|
||||
) -> Set[Tuple[str, int]]:
|
||||
"""
|
||||
Scan a list of IPs across given ports as fast as possible.
|
||||
Uses asyncio semaphore to control concurrency.
|
||||
|
||||
- progress_callback(checked, total, live_count, rate): called periodically
|
||||
- live_callback(ip, port): called IMMEDIATELY when a live host is found
|
||||
|
||||
Returns set of (ip, port) tuples for live RDP hosts.
|
||||
"""
|
||||
if ports is None:
|
||||
ports = RDP_PORTS
|
||||
|
||||
sem = asyncio.Semaphore(max_concurrent)
|
||||
live_hosts: Set[Tuple[str, int]] = set()
|
||||
checked = 0
|
||||
total = len(ips) * len(ports)
|
||||
start_time = time.time()
|
||||
|
||||
checker = check_rdp_with_banner if banner_check else check_rdp_port
|
||||
|
||||
async def check_one(ip: str, port: int):
|
||||
nonlocal checked
|
||||
async with sem:
|
||||
_, p, alive = await checker(ip, port, connect_timeout)
|
||||
if alive:
|
||||
live_hosts.add((ip, p))
|
||||
# Notify immediately — spray can start right away
|
||||
if live_callback:
|
||||
live_callback(ip, p)
|
||||
checked += 1
|
||||
if progress_callback and checked % 1000 == 0:
|
||||
elapsed = time.time() - start_time
|
||||
rate = checked / elapsed if elapsed > 0 else 0
|
||||
progress_callback(checked, total, len(live_hosts), rate)
|
||||
|
||||
# Fire all tasks concurrently
|
||||
tasks = []
|
||||
for ip in ips:
|
||||
for port in ports:
|
||||
tasks.append(asyncio.create_task(check_one(ip, port)))
|
||||
|
||||
# Run in batches to avoid memory issues with massive task lists
|
||||
batch_size = 50000
|
||||
for i in range(0, len(tasks), batch_size):
|
||||
batch = tasks[i:i + batch_size]
|
||||
if batch:
|
||||
await asyncio.gather(*batch, return_exceptions=True)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
rate = checked / elapsed if elapsed > 0 else 0
|
||||
logger.info(
|
||||
f"Scan complete: {len(live_hosts)} live hosts from {checked} checks "
|
||||
f"in {elapsed:.1f}s ({rate:.0f} checks/sec)"
|
||||
)
|
||||
|
||||
return live_hosts
|
||||
Reference in New Issue
Block a user