|
| 1 | +#!/usr/bin/env python3 |
| 2 | +# -*- coding: utf-8 -*- |
| 3 | +""" |
| 4 | +cdn_path_finder.py — لایه ۲: یافتن IP های باز CDN برای عبور از فیلتر |
| 5 | +
|
| 6 | +این ماژول IP های CDN هایی را که دولتها باز نگه میدارند |
| 7 | +اسکن میکند تا مسیری برای relay کردن ترافیک پیدا کند. |
| 8 | +
|
| 9 | +استفاده: |
| 10 | + python3 cdn_path_finder.py --scan-cloudflare --target your-server.com |
| 11 | + python3 cdn_path_finder.py --scan-all --target your-server.com |
| 12 | +""" |
| 13 | + |
| 14 | +import ipaddress |
| 15 | +import json |
| 16 | +import random |
| 17 | +import socket |
| 18 | +import ssl |
| 19 | +import struct |
| 20 | +import sys |
| 21 | +import threading |
| 22 | +import time |
| 23 | +import urllib.request |
| 24 | +from concurrent.futures import ThreadPoolExecutor, as_completed |
| 25 | +from typing import Dict, List, Optional, Tuple |
| 26 | + |
| 27 | +CONNECT_TIMEOUT = 3.0 |
| 28 | +HTTP_TIMEOUT = 5.0 |
| 29 | +MAX_WORKERS = 64 |
| 30 | +RESULTS_FILE = "cdn_open_ips.json" |
| 31 | + |
| 32 | +# ───────────────────────────────────────────────────────────── |
| 33 | +# رنجهای IP عمومی CDN های اصلی |
| 34 | +# منبع: https://www.cloudflare.com/ips/ |
| 35 | +# https://api.fastly.com/public-ip-list |
| 36 | +# https://github.com/nicholasMeadows/AkamaiIPRanges |
| 37 | +# ───────────────────────────────────────────────────────────── |
| 38 | +CDN_RANGES: Dict[str, List[str]] = { |
| 39 | + "cloudflare": [ |
| 40 | + "103.21.244.0/22", "103.22.200.0/22", "103.31.4.0/22", |
| 41 | + "104.16.0.0/13", "104.24.0.0/14", "108.162.192.0/18", |
| 42 | + "131.0.72.0/22", "141.101.64.0/18", "162.158.0.0/15", |
| 43 | + "172.64.0.0/13", "173.245.48.0/20", "188.114.96.0/20", |
| 44 | + "190.93.240.0/20", "197.234.240.0/22", "198.41.128.0/17", |
| 45 | + ], |
| 46 | + "fastly": [ |
| 47 | + "23.235.32.0/20", "43.249.72.0/22", "103.244.50.0/24", |
| 48 | + "103.245.222.0/23","103.245.224.0/24","104.156.80.0/20", |
| 49 | + "140.248.64.0/18", "140.248.128.0/17","146.75.0.0/17", |
| 50 | + "151.101.0.0/16", "157.52.192.0/18", "167.82.0.0/17", |
| 51 | + "167.82.128.0/20", "167.82.160.0/20", "167.82.224.0/20", |
| 52 | + "172.111.64.0/18", "185.31.16.0/22", "199.27.72.0/21", |
| 53 | + "199.232.0.0/16", |
| 54 | + ], |
| 55 | + "akamai": [ |
| 56 | + "2.16.0.0/13", "23.0.0.0/12", "23.192.0.0/11", |
| 57 | + "69.192.0.0/16", "72.246.0.0/15", "88.221.0.0/16", |
| 58 | + "92.122.0.0/15", "96.6.0.0/15", "184.24.0.0/13", |
| 59 | + "184.50.0.0/15", "184.84.0.0/14", |
| 60 | + ], |
| 61 | + "bunny": [ |
| 62 | + "185.181.48.0/22", "192.189.0.0/16", |
| 63 | + ], |
| 64 | +} |
| 65 | + |
| 66 | +# ───────────────────────────────────────────────────────────── |
| 67 | +# رنگبندی ترمینال |
| 68 | +# ───────────────────────────────────────────────────────────── |
| 69 | +def _c(text: str, code: str) -> str: |
| 70 | + if not sys.stdout.isatty(): |
| 71 | + return text |
| 72 | + return f"\033[{code}m{text}\033[0m" |
| 73 | + |
| 74 | +ok = lambda t: _c(t, "92") |
| 75 | +deny = lambda t: _c(t, "91") |
| 76 | +warn = lambda t: _c(t, "93") |
| 77 | +info = lambda t: _c(t, "96") |
| 78 | + |
| 79 | + |
| 80 | +# ───────────────────────────────────────────────────────────── |
| 81 | +# یافتن IP های باز |
| 82 | +# ───────────────────────────────────────────────────────────── |
| 83 | +def tcp_open(ip: str, port: int = 443) -> bool: |
| 84 | + """بررسی اینکه پورت TCP روی این IP باز است.""" |
| 85 | + try: |
| 86 | + with socket.create_connection((ip, port), timeout=CONNECT_TIMEOUT): |
| 87 | + return True |
| 88 | + except Exception: |
| 89 | + return False |
| 90 | + |
| 91 | + |
| 92 | +def https_reachable(ip: str, host_header: str = "", path: str = "/") -> Tuple[bool, int, float]: |
| 93 | + """ |
| 94 | + یک درخواست HTTPS به IP میفرستد و کد پاسخ + تأخیر را برمیگرداند. |
| 95 | + host_header میتواند برای domain fronting استفاده شود. |
| 96 | + """ |
| 97 | + ctx = ssl.create_default_context() |
| 98 | + ctx.check_hostname = False |
| 99 | + ctx.verify_mode = ssl.CERT_NONE |
| 100 | + t0 = time.monotonic() |
| 101 | + try: |
| 102 | + conn = socket.create_connection((ip, 443), timeout=HTTP_TIMEOUT) |
| 103 | + tls = ctx.wrap_socket(conn, server_hostname=host_header or ip) |
| 104 | + header = host_header or ip |
| 105 | + req = ( |
| 106 | + f"GET {path} HTTP/1.1\r\n" |
| 107 | + f"Host: {header}\r\n" |
| 108 | + "User-Agent: curl/7.88.1\r\n" |
| 109 | + "Accept: */*\r\n" |
| 110 | + "Connection: close\r\n\r\n" |
| 111 | + ).encode() |
| 112 | + tls.sendall(req) |
| 113 | + resp = tls.recv(4096).decode(errors="ignore") |
| 114 | + tls.close() |
| 115 | + latency = (time.monotonic() - t0) * 1000 |
| 116 | + code = 0 |
| 117 | + first = resp.split("\r\n", 1)[0] |
| 118 | + if "HTTP/" in first: |
| 119 | + try: |
| 120 | + code = int(first.split()[1]) |
| 121 | + except Exception: |
| 122 | + pass |
| 123 | + return True, code, latency |
| 124 | + except Exception: |
| 125 | + return False, 0, (time.monotonic() - t0) * 1000 |
| 126 | + |
| 127 | + |
| 128 | +def expand_range(cidr: str, max_ips: int = 256) -> List[str]: |
| 129 | + """یک رنج CIDR را به لیست IP تبدیل میکند (با shuffle برای تنوع).""" |
| 130 | + net = ipaddress.ip_network(cidr, strict=False) |
| 131 | + ips = [str(h) for h in net.hosts()] |
| 132 | + if len(ips) > max_ips: |
| 133 | + random.shuffle(ips) |
| 134 | + ips = ips[:max_ips] |
| 135 | + return ips |
| 136 | + |
| 137 | + |
| 138 | +def scan_cdn( |
| 139 | + cdn_name: str, |
| 140 | + ranges: List[str], |
| 141 | + port: int = 443, |
| 142 | + host_header: str = "", |
| 143 | + max_ips: int = 50, |
| 144 | + workers: int = MAX_WORKERS, |
| 145 | +) -> List[Dict]: |
| 146 | + """ |
| 147 | + IP های یک CDN را اسکن میکند و نتایج را بر اساس تأخیر مرتب میکند. |
| 148 | + """ |
| 149 | + all_ips: List[str] = [] |
| 150 | + for cidr in ranges: |
| 151 | + all_ips.extend(expand_range(cidr, max_ips // len(ranges) + 1)) |
| 152 | + random.shuffle(all_ips) |
| 153 | + all_ips = all_ips[:max_ips] |
| 154 | + |
| 155 | + results = [] |
| 156 | + lock = threading.Lock() |
| 157 | + |
| 158 | + def probe(ip: str): |
| 159 | + reachable, code, latency = https_reachable(ip, host_header, "/") |
| 160 | + if reachable: |
| 161 | + entry = {"ip": ip, "cdn": cdn_name, "code": code, "latency_ms": round(latency, 1)} |
| 162 | + with lock: |
| 163 | + results.append(entry) |
| 164 | + print(f" {ok('✓')} {ip:>16} HTTP {code} {latency:6.0f} ms") |
| 165 | + |
| 166 | + print(info(f"\n[{cdn_name}] اسکن {len(all_ips)} IP روی پورت {port} ...")) |
| 167 | + with ThreadPoolExecutor(max_workers=workers) as ex: |
| 168 | + futures = {ex.submit(probe, ip): ip for ip in all_ips} |
| 169 | + for f in as_completed(futures): |
| 170 | + _ = f # خطاها داخل probe مدیریت میشوند |
| 171 | + |
| 172 | + results.sort(key=lambda r: r["latency_ms"]) |
| 173 | + return results |
| 174 | + |
| 175 | + |
| 176 | +# ───────────────────────────────────────────────────────────── |
| 177 | +# Domain Fronting — پوشش ترافیک با یک دامنه مجاز |
| 178 | +# ───────────────────────────────────────────────────────────── |
| 179 | +def domain_front_test( |
| 180 | + cdn_ip: str, |
| 181 | + front_domain: str, |
| 182 | + real_host: str, |
| 183 | + path: str = "/", |
| 184 | +) -> Tuple[bool, int, str]: |
| 185 | + """ |
| 186 | + Domain Fronting: |
| 187 | + - SNI و Host را front_domain میگذارد (دامنهای که فیلتر نیست) |
| 188 | + - اما درخواست به real_host هدایت میشود (اگر هر دو روی یک CDN باشند) |
| 189 | + |
| 190 | + این تکنیک روی CDN هایی که allow میکنند کار میکند. |
| 191 | + """ |
| 192 | + ctx = ssl.create_default_context() |
| 193 | + ctx.check_hostname = False |
| 194 | + ctx.verify_mode = ssl.CERT_NONE |
| 195 | + try: |
| 196 | + conn = socket.create_connection((cdn_ip, 443), timeout=HTTP_TIMEOUT) |
| 197 | + # SNI = front_domain (دامنه غیر فیلتر) |
| 198 | + tls = ctx.wrap_socket(conn, server_hostname=front_domain) |
| 199 | + # Host header = real_host (سرور واقعی شما) |
| 200 | + req = ( |
| 201 | + f"GET {path} HTTP/1.1\r\n" |
| 202 | + f"Host: {real_host}\r\n" |
| 203 | + "User-Agent: curl/7.88.1\r\n" |
| 204 | + "Connection: close\r\n\r\n" |
| 205 | + ).encode() |
| 206 | + tls.sendall(req) |
| 207 | + resp = tls.recv(4096).decode(errors="ignore") |
| 208 | + tls.close() |
| 209 | + code = 0 |
| 210 | + first = resp.split("\r\n", 1)[0] |
| 211 | + if "HTTP/" in first: |
| 212 | + try: |
| 213 | + code = int(first.split()[1]) |
| 214 | + except Exception: |
| 215 | + pass |
| 216 | + success = code in (200, 301, 302, 204) |
| 217 | + return success, code, resp.split("\r\n")[0] |
| 218 | + except Exception as e: |
| 219 | + return False, 0, str(e) |
| 220 | + |
| 221 | + |
| 222 | +# ───────────────────────────────────────────────────────────── |
| 223 | +# Cloudflare Worker به عنوان relay پروکسی |
| 224 | +# ───────────────────────────────────────────────────────────── |
| 225 | +WORKER_SCRIPT_TEMPLATE = """\ |
| 226 | +// Cloudflare Worker — relay proxy |
| 227 | +// در داشبورد Cloudflare Workers ایجاد کنید |
| 228 | +// این worker درخواست را به سرور شما forward میکند |
| 229 | +
|
| 230 | +const TARGET = "https://{target_host}"; |
| 231 | +
|
| 232 | +export default {{ |
| 233 | + async fetch(request, env) {{ |
| 234 | + const url = new URL(request.url); |
| 235 | + const targetUrl = TARGET + url.pathname + url.search; |
| 236 | + |
| 237 | + const newRequest = new Request(targetUrl, {{ |
| 238 | + method: request.method, |
| 239 | + headers: request.headers, |
| 240 | + body: request.body, |
| 241 | + }}); |
| 242 | + |
| 243 | + return fetch(newRequest); |
| 244 | + }}, |
| 245 | +}}; |
| 246 | +""" |
| 247 | + |
| 248 | +def generate_worker_script(target_host: str) -> str: |
| 249 | + return WORKER_SCRIPT_TEMPLATE.format(target_host=target_host) |
| 250 | + |
| 251 | + |
| 252 | +# ───────────────────────────────────────────────────────────── |
| 253 | +# ذخیره و بارگذاری نتایج |
| 254 | +# ───────────────────────────────────────────────────────────── |
| 255 | +def save_results(results: List[Dict], path: str = RESULTS_FILE) -> None: |
| 256 | + with open(path, "w", encoding="utf-8") as f: |
| 257 | + json.dump(results, f, indent=2, ensure_ascii=False) |
| 258 | + print(info(f"\n[ذخیره] {len(results)} IP در {path}")) |
| 259 | + |
| 260 | + |
| 261 | +def load_results(path: str = RESULTS_FILE) -> List[Dict]: |
| 262 | + try: |
| 263 | + with open(path, encoding="utf-8") as f: |
| 264 | + return json.load(f) |
| 265 | + except Exception: |
| 266 | + return [] |
| 267 | + |
| 268 | + |
| 269 | +def best_ips(results: List[Dict], top_n: int = 5) -> List[Dict]: |
| 270 | + """سریعترین IP های پیدا شده را برمیگرداند.""" |
| 271 | + return sorted(results, key=lambda r: r["latency_ms"])[:top_n] |
| 272 | + |
| 273 | + |
| 274 | +# ───────────────────────────────────────────────────────────── |
| 275 | +# رابط خط فرمان |
| 276 | +# ───────────────────────────────────────────────────────────── |
| 277 | +def main() -> int: |
| 278 | + import argparse |
| 279 | + p = argparse.ArgumentParser(description="یافتن IP های باز CDN برای عبور از فیلتر") |
| 280 | + p.add_argument("--scan-cloudflare", action="store_true", help="اسکن IP های Cloudflare") |
| 281 | + p.add_argument("--scan-fastly", action="store_true", help="اسکن IP های Fastly") |
| 282 | + p.add_argument("--scan-akamai", action="store_true", help="اسکن IP های Akamai") |
| 283 | + p.add_argument("--scan-all", action="store_true", help="اسکن همه CDN ها") |
| 284 | + p.add_argument("--target", default="", help="دامنه سرور شما") |
| 285 | + p.add_argument("--front", default="", help="دامنه front برای domain fronting") |
| 286 | + p.add_argument("--max-ips", type=int, default=60,help="حداکثر IP برای اسکن هر CDN") |
| 287 | + p.add_argument("--show-best", action="store_true", help="نمایش بهترین IP های ذخیرهشده") |
| 288 | + p.add_argument("--gen-worker", action="store_true", help="تولید اسکریپت Cloudflare Worker") |
| 289 | + args = p.parse_args() |
| 290 | + |
| 291 | + if args.show_best: |
| 292 | + results = load_results() |
| 293 | + top = best_ips(results, 10) |
| 294 | + print(info(f"\n{'IP':>18} {'CDN':<12} {'Code':>5} {'Latency':>10}")) |
| 295 | + print("-" * 56) |
| 296 | + for r in top: |
| 297 | + print(f" {r['ip']:>16} {r['cdn']:<12} {r['code']:>5} {r['latency_ms']:>8.0f} ms") |
| 298 | + return 0 |
| 299 | + |
| 300 | + if args.gen_worker: |
| 301 | + if not args.target: |
| 302 | + print(deny("[خطا] --target لازم است")) |
| 303 | + return 1 |
| 304 | + script = generate_worker_script(args.target) |
| 305 | + fname = "worker_relay.js" |
| 306 | + with open(fname, "w") as f: |
| 307 | + f.write(script) |
| 308 | + print(ok(f"[Worker] اسکریپت در {fname} ذخیره شد")) |
| 309 | + print(info("در داشبورد Cloudflare Workers → Create Worker بارگذاری کنید")) |
| 310 | + return 0 |
| 311 | + |
| 312 | + to_scan = [] |
| 313 | + if args.scan_all or args.scan_cloudflare: |
| 314 | + to_scan.append(("cloudflare", CDN_RANGES["cloudflare"])) |
| 315 | + if args.scan_all or args.scan_fastly: |
| 316 | + to_scan.append(("fastly", CDN_RANGES["fastly"])) |
| 317 | + if args.scan_all or args.scan_akamai: |
| 318 | + to_scan.append(("akamai", CDN_RANGES["akamai"])) |
| 319 | + |
| 320 | + if not to_scan: |
| 321 | + p.print_help() |
| 322 | + return 1 |
| 323 | + |
| 324 | + all_results = [] |
| 325 | + for cdn_name, ranges in to_scan: |
| 326 | + found = scan_cdn( |
| 327 | + cdn_name, ranges, |
| 328 | + host_header=args.front or args.target, |
| 329 | + max_ips=args.max_ips, |
| 330 | + ) |
| 331 | + all_results.extend(found) |
| 332 | + print(info(f"[{cdn_name}] {len(found)} IP باز پیدا شد")) |
| 333 | + |
| 334 | + if all_results: |
| 335 | + save_results(all_results) |
| 336 | + print(info("\nبهترین IP ها:")) |
| 337 | + for r in best_ips(all_results, 5): |
| 338 | + print(f" {ok('✓')} {r['ip']:>16} {r['cdn']:<12} {r['latency_ms']:.0f} ms") |
| 339 | + |
| 340 | + # تست domain fronting اگر target و front داده شده |
| 341 | + if args.target and args.front and all_results: |
| 342 | + best = all_results[0]["ip"] |
| 343 | + print(info(f"\n[Domain Fronting] تست با {best} ...")) |
| 344 | + success, code, first_line = domain_front_test( |
| 345 | + best, args.front, args.target |
| 346 | + ) |
| 347 | + if success: |
| 348 | + print(ok(f" ✓ Domain fronting کار کرد! HTTP {code}")) |
| 349 | + else: |
| 350 | + print(warn(f" ⚠ Domain fronting کار نکرد: {first_line}")) |
| 351 | + |
| 352 | + return 0 |
| 353 | + |
| 354 | + |
| 355 | +if __name__ == "__main__": |
| 356 | + raise SystemExit(main()) |
0 commit comments