- Add REAPER.bat single entry (pip, dirs, smoke test, launch); MASTER/MASTERSTER shim to it - bruteforce: rdpthread helpers, safe writer close, spray_all_hosts incremental writes, progress - gui: credential banner, spray messaging without spam; cmdkey TERMSRV host - scanner: adaptive progress; proxy: retry cap on open_connection - ip_utils: /32 CIDR, count_ips aligned with large dash ranges - README/install/run: deployment docs and REAPER.bat references Co-authored-by: Cursor <cursoragent@cursor.com>
97 lines
3.1 KiB
Python
97 lines
3.1 KiB
Python
"""
|
|
FastRDP-NG IP Utilities
|
|
Parse IP ranges, CIDR notation, stick to basics.
|
|
"""
|
|
|
|
import ipaddress
|
|
import re
|
|
from typing import Generator
|
|
|
|
IP_RANGE_RE = re.compile(
|
|
r'^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})\s*-\s*(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$'
|
|
)
|
|
SINGLE_IP_RE = re.compile(r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$')
|
|
|
|
|
|
def parse_line(line: str) -> Generator[str, None, None]:
|
|
"""Parse a single line: single IP, dash range, CIDR."""
|
|
line = line.strip()
|
|
if not line or line.startswith('#') or line.startswith('//'):
|
|
return
|
|
|
|
# CIDR: 192.168.1.0/24 (/32 must use the network address — .hosts() is empty)
|
|
if '/' in line:
|
|
try:
|
|
network = ipaddress.IPv4Network(line, strict=False)
|
|
if network.prefixlen == 32:
|
|
yield str(network.network_address)
|
|
else:
|
|
for host in network.hosts():
|
|
yield str(host)
|
|
except ValueError:
|
|
pass
|
|
return
|
|
|
|
# Dash range: 192.168.1.1-192.168.1.255
|
|
match = IP_RANGE_RE.match(line)
|
|
if match:
|
|
try:
|
|
start = int(ipaddress.IPv4Address(match.group(1)))
|
|
end = int(ipaddress.IPv4Address(match.group(2)))
|
|
if start > end:
|
|
start, end = end, start
|
|
# Cap at /16 to prevent memory bombs
|
|
if end - start > 65536:
|
|
end = start + 65536
|
|
for ip_int in range(start, end + 1):
|
|
yield str(ipaddress.IPv4Address(ip_int))
|
|
except ValueError:
|
|
pass
|
|
return
|
|
|
|
# Single IP
|
|
if SINGLE_IP_RE.match(line):
|
|
yield line
|
|
|
|
|
|
def parse_ranges_file(filepath: str) -> Generator[str, None, None]:
|
|
"""Parse ranges.txt, yield individual IPs lazily."""
|
|
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
for line in f:
|
|
yield from parse_line(line)
|
|
|
|
|
|
def count_ips(filepath: str) -> int:
|
|
"""Quick count of total IPs for progress tracking."""
|
|
total = 0
|
|
with open(filepath, 'r', encoding='utf-8', errors='ignore') as f:
|
|
for line in f:
|
|
line = line.strip()
|
|
if not line or line.startswith('#') or line.startswith('//'):
|
|
continue
|
|
if '/' in line:
|
|
try:
|
|
n = ipaddress.IPv4Network(line, strict=False)
|
|
if n.prefixlen >= 31:
|
|
total += n.num_addresses
|
|
else:
|
|
total += max(0, n.num_addresses - 2)
|
|
except ValueError:
|
|
total += 1
|
|
elif IP_RANGE_RE.match(line):
|
|
m = IP_RANGE_RE.match(line)
|
|
if m is None:
|
|
total += 1
|
|
continue
|
|
s = int(ipaddress.IPv4Address(m.group(1)))
|
|
e = int(ipaddress.IPv4Address(m.group(2)))
|
|
if s > e:
|
|
s, e = e, s
|
|
# Same /16 cap as parse_line() so Total matches work done
|
|
if e - s > 65536:
|
|
e = s + 65536
|
|
total += e - s + 1
|
|
elif SINGLE_IP_RE.match(line):
|
|
total += 1
|
|
return total
|