diff --git a/.gitignore b/.gitignore index b7faf40..f25dd42 100644 --- a/.gitignore +++ b/.gitignore @@ -205,3 +205,4 @@ cython_debug/ marimo/_static/ marimo/_lsp/ __marimo__/ +*.lnk diff --git a/config.json b/config.json index 189ba03..ed8701e 100644 --- a/config.json +++ b/config.json @@ -1,7 +1,7 @@ { "LISTEN_HOST": "0.0.0.0", "LISTEN_PORT": 40443, - "CONNECT_IP": "188.114.98.0", + "CONNECT_IP": "188.114.99.0", "CONNECT_PORT": 443, - "FAKE_SNI": "auth.vercel.com" + "FAKE_SNI": "chatgpt.com" } \ No newline at end of file diff --git a/fake_tcp.py b/fake_tcp.py index 8baf26c..01f3af5 100644 --- a/fake_tcp.py +++ b/fake_tcp.py @@ -3,6 +3,7 @@ import sys import threading import time +from concurrent.futures import ThreadPoolExecutor from pydivert import Packet @@ -29,6 +30,7 @@ class FakeTcpInjector(TcpInjector): def __init__(self, w_filter: str, connections: dict[tuple, FakeInjectiveConnection]): super().__init__(w_filter) self.connections = connections + self.executor = ThreadPoolExecutor(max_workers=64) def fake_send_thread(self, packet: Packet, connection: FakeInjectiveConnection): time.sleep(0.001) @@ -143,7 +145,7 @@ def on_outbound_packet(self, packet: Packet, connection: FakeInjectiveConnection self.w.send(packet, False) connection.sch_fake_sent = True - threading.Thread(target=self.fake_send_thread, args=(packet, connection), daemon=True).start() + self.executor.submit(self.fake_send_thread, packet, connection) return self.on_unexpected_packet(packet, connection, "unexpected outbound packet") return diff --git a/main.py b/main.py index 3170b25..1facb38 100644 --- a/main.py +++ b/main.py @@ -5,6 +5,9 @@ import traceback import threading import json +import ctypes +import subprocess +import time # from utils.proxy_protocols import parse_vless_protocol from utils.network_tools import get_default_interface_ipv4 @@ -22,19 +25,55 @@ def get_exe_dir(): return os.path.dirname(os.path.abspath(__file__)) -# Build the path to config.json +# Load or create the config config_path = os.path.join(get_exe_dir(), 'config.json') +if not os.path.exists(config_path): + default_config = { + "LISTEN_HOST": "0.0.0.0", + "LISTEN_PORT": 40443, + "CONNECT_IP": "188.114.99.0", + "CONNECT_PORT": 443, + "FAKE_SNI": "chatgpt.com" + } + try: + with open(config_path, 'w') as f: + json.dump(default_config, f, indent=2) + print(f"[Info] Created default config.json at {config_path}") + config = default_config + except Exception as e: + print(f"[Warning] Could not write default config.json: {e}") + config = default_config +else: + try: + with open(config_path, 'r') as f: + config = json.load(f) + except Exception as e: + print(f"[Error] Failed to read config.json: {e}. Using default configuration.") + config = { + "LISTEN_HOST": "0.0.0.0", + "LISTEN_PORT": 40443, + "CONNECT_IP": "188.114.99.0", + "CONNECT_PORT": 443, + "FAKE_SNI": "chatgpt.com" + } -# Load the config -with open(config_path, 'r') as f: - config = json.load(f) - -LISTEN_HOST = config["LISTEN_HOST"] -LISTEN_PORT = config["LISTEN_PORT"] -FAKE_SNI = config["FAKE_SNI"].encode() -CONNECT_IP = config["CONNECT_IP"] -CONNECT_PORT = config["CONNECT_PORT"] +LISTEN_HOST = config.get("LISTEN_HOST", "0.0.0.0") +LISTEN_PORT = config.get("LISTEN_PORT", 40443) +FAKE_SNI = config.get("FAKE_SNI", "chatgpt.com").encode() +CONNECT_IP = config.get("CONNECT_IP", "188.114.99.0") +CONNECT_PORT = config.get("CONNECT_PORT", 443) INTERFACE_IPV4 = get_default_interface_ipv4(CONNECT_IP) + + +def check_windivert_binaries(): + """Helper to check if WinDivert binaries are present in script or workdir.""" + if os.name == 'nt': + exe_dir = get_exe_dir() + wd_files = ["WinDivert.dll", "WinDivert64.sys"] + missing = [f for f in wd_files if not os.path.exists(os.path.join(exe_dir, f)) and not os.path.exists(os.path.join(os.getcwd(), f))] + if missing: + print(f"[Note] WinDivert binaries ({', '.join(missing)}) were not found in the application directory.") + print(" If the application fails to start packet capture, please download WinDivert and place them here.") DATA_MODE = "tls" BYPASS_METHOD = "wrong_seq" @@ -51,24 +90,30 @@ async def relay_main_loop(sock_1: socket.socket, sock_2: socket.socket, peer_tas try: data = await loop.sock_recv(sock_1, 65575) if not data: - raise ValueError("eof") + break if first_prefix_data: data = first_prefix_data + data first_prefix_data = b"" - sent_len = await loop.sock_sendall(sock_2, data) - if sent_len != len(data): - raise ValueError("incomplete send") - except Exception: - sock_1.close() - sock_2.close() - peer_task.cancel() - return + await loop.sock_sendall(sock_2, data) + except (ConnectionResetError, OSError, asyncio.CancelledError): + break except Exception: traceback.print_exc() sys.exit("relay main loop error!") + finally: + if peer_task and not peer_task.done(): + for sock in (sock_1, sock_2): + try: + sock.shutdown(socket.SHUT_RDWR) + except OSError: + pass + sock_1.close() + sock_2.close() async def handle(incoming_sock: socket.socket, incoming_remote_addr): + conn_id = f"{incoming_remote_addr[0]}:{incoming_remote_addr[1]}" + print(f"[+] Client connected: {conn_id}") try: loop = asyncio.get_running_loop() # try: @@ -112,14 +157,14 @@ async def handle(incoming_sock: socket.socket, incoming_remote_addr): # else: # print(data) # sys.exit("impossible address type!") - + # try: # fake_sni_host, data_mode, bypass_method = UUID_FAKE_MAP[uuid_bytes] # except KeyError: # print("unmatched uuid", uuid_bytes) # incoming_sock.close() # return - + # if data_mode == "http": # ... if DATA_MODE == "tls": @@ -140,58 +185,41 @@ async def handle(incoming_sock: socket.socket, incoming_remote_addr): BYPASS_METHOD, incoming_sock) fake_injective_connections[fake_injective_conn.id] = fake_injective_conn try: - await loop.sock_connect(outgoing_sock, (CONNECT_IP, CONNECT_PORT)) - except Exception: - fake_injective_conn.monitor = False - del fake_injective_connections[fake_injective_conn.id] - outgoing_sock.close() - incoming_sock.close() - return - - # if bypass_method == "wrong_checksum": - # ... - - if BYPASS_METHOD == "wrong_seq": try: - await asyncio.wait_for(fake_injective_conn.t2a_event.wait(), 2) - if fake_injective_conn.t2a_msg == "unexpected_close": - raise ValueError("unexpected close") - if fake_injective_conn.t2a_msg == "fake_data_ack_recv": - pass - else: - sys.exit("impossible t2a msg!") + await loop.sock_connect(outgoing_sock, (CONNECT_IP, CONNECT_PORT)) except Exception: - fake_injective_conn.monitor = False - del fake_injective_connections[fake_injective_conn.id] outgoing_sock.close() incoming_sock.close() return - else: - sys.exit("unknown bypass method!") - - fake_injective_conn.monitor = False - del fake_injective_connections[fake_injective_conn.id] - - # early_data = data[payload_index:] - # if early_data: - # try: - # sent_len = await loop.sock_sendall(outgoing_sock, early_data) - # if sent_len != len(early_data): - # raise ValueError("incomplete send") - # except Exception: - # outgoing_sock.close() - # incoming_sock.close() - # return + + if BYPASS_METHOD == "wrong_seq": + try: + await asyncio.wait_for(fake_injective_conn.t2a_event.wait(), 2) + if fake_injective_conn.t2a_msg == "unexpected_close": + raise ValueError("unexpected close") + if fake_injective_conn.t2a_msg == "fake_data_ack_recv": + pass + else: + sys.exit("impossible t2a msg!") + except Exception: + outgoing_sock.close() + incoming_sock.close() + return + else: + sys.exit("unknown bypass method!") + finally: + fake_injective_conn.monitor = False + fake_injective_connections.pop(fake_injective_conn.id, None) oti_task = asyncio.create_task( relay_main_loop(outgoing_sock, incoming_sock, asyncio.current_task(), b"")) # bytes([version, 0]) await relay_main_loop(incoming_sock, outgoing_sock, oti_task, b"") - - - + except Exception: traceback.print_exc() sys.exit("handle should not raise exception") + finally: + print(f"[-] Client disconnected: {conn_id}") async def main(): @@ -215,14 +243,314 @@ async def main(): asyncio.create_task(handle(incoming_sock, addr)) +def is_admin() -> bool: + if os.name == 'nt': + try: + return ctypes.windll.shell32.IsUserAnAdmin() != 0 + except Exception: + return False + else: + try: + return os.getuid() == 0 + except AttributeError: + return False + + +def run_as_admin(): + if os.name == 'nt': + try: + if getattr(sys, 'frozen', False): + # Executable mode + ret = ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, " ".join(sys.argv[1:]), None, 1 + ) + else: + # Script mode + script = sys.argv[0] + params = f'"{script}" ' + " ".join([f'"{arg}"' for arg in sys.argv[1:]]) + ret = ctypes.windll.shell32.ShellExecuteW( + None, "runas", sys.executable, params, None, 1 + ) + if int(ret) <= 32: + print("Administrator privileges were denied. Exiting.") + sys.exit(1) + else: + sys.exit(0) + except Exception as e: + print(f"Failed to elevate privileges: {e}") + sys.exit(1) + else: + print("Please run this script as root/administrator.") + sys.exit(1) + + +def get_adapters(): + # Returns a list of dictionaries: [{'IPAddress': '...', 'InterfaceAlias': '...'}] + cmd = ["powershell", "-Command", "Get-NetIPAddress -AddressFamily IPv4 | Select-Object IPAddress, InterfaceAlias | ConvertTo-Json"] + try: + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + if res.stdout.strip(): + data = json.loads(res.stdout) + if isinstance(data, dict): + return [data] + return data + except Exception: + pass + return [] + + +def get_real_default_interface(connect_ip="8.8.8.8") -> tuple[str, str]: + # Returns (InterfaceAlias, IPAddress) + adapters = get_adapters() + ip_to_name = {a['IPAddress']: a['InterfaceAlias'] for a in adapters if 'IPAddress' in a and 'InterfaceAlias' in a} + name_to_ips = {} + for a in adapters: + if 'IPAddress' in a and 'InterfaceAlias' in a: + name_to_ips.setdefault(a['InterfaceAlias'], []).append(a['IPAddress']) + + def is_proxy_tun(name: str) -> bool: + name_lower = name.lower() + return any(x in name_lower for x in ["xray", "sing", "wintun", "tun", "tap", "loopback", "pseudo"]) + + routes = [] + cmd = ["powershell", "-Command", "Get-NetRoute -DestinationPrefix '0.0.0.0/0' | Select-Object NextHop, InterfaceAlias, RouteMetric | ConvertTo-Json"] + try: + res = subprocess.run(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, check=True) + if res.stdout.strip(): + routes_data = json.loads(res.stdout) + if isinstance(routes_data, dict): + routes = [routes_data] + elif isinstance(routes_data, list): + routes = routes_data + except Exception: + pass + + physical_routes = [] + for r in routes: + name = r.get('InterfaceAlias', '') + if name and not is_proxy_tun(name): + physical_routes.append(r) + + if physical_routes: + physical_routes.sort(key=lambda x: x.get('RouteMetric', 9999)) + best_name = physical_routes[0]['InterfaceAlias'] + ips = name_to_ips.get(best_name, []) + for ip in ips: + if not ip.startswith("169.254") and not ip.startswith("127."): + return best_name, ip + + for name, ips in name_to_ips.items(): + if not is_proxy_tun(name): + for ip in ips: + if not ip.startswith("169.254") and not ip.startswith("127."): + return name, ip + + try: + default_ip = get_default_interface_ipv4(connect_ip) + if default_ip: + name = ip_to_name.get(default_ip, "Unknown") + return name, default_ip + except Exception: + pass + + return "Unknown", "" + + +def select_network_interface() -> tuple[str, str]: + adapters = get_adapters() + adapter_ips = {} + for a in adapters: + name = a.get('InterfaceAlias') + ip = a.get('IPAddress') + if name and ip: + adapter_ips.setdefault(name, []).append(ip) + + # Fallback if powershell command returned nothing (e.g. not on Windows) + if not adapter_ips: + try: + hostname = socket.gethostname() + ips = socket.gethostbyname_ex(hostname)[2] + adapter_ips['Default Adapter'] = ips + except Exception: + pass + + if 'Loopback Pseudo-Interface 1' not in adapter_ips and 'Loopback' not in adapter_ips: + for name in list(adapter_ips.keys()): + if 'loopback' in name.lower() or 'pseudo' in name.lower(): + break + else: + adapter_ips['Loopback Pseudo-Interface 1'] = ['127.0.0.1'] + + default_name, default_ip = get_real_default_interface(CONNECT_IP) + + def is_proxy_tun(n: str) -> bool: + n_lower = n.lower() + return any(x in n_lower for x in ["xray", "sing", "wintun", "tun", "tap", "loopback", "pseudo"]) + + print("\n==================================================") + print("Available Network Interfaces:") + names_list = list(adapter_ips.keys()) + for idx, name in enumerate(names_list, 1): + ips = adapter_ips[name] + ip_str = ", ".join(ips) + is_default = " (Default)" if name == default_name else "" + + # Determine label type + if name == default_name: + label = "[Physical/Active Default]" + elif "loopback" in name.lower() or "pseudo" in name.lower(): + label = "[Loopback]" + elif is_proxy_tun(name): + label = "[TUN/VPN/Virtual]" + else: + label = "[Other/LAN]" + + print(f" {idx}. {name:<30} -> {ip_str:<20} {label}{is_default}") + print("==================================================\n") + + default_prompt = f" [Default: {default_name}]" if default_name else "" + while True: + try: + choice = input(f"Select network interface (1-{len(names_list)}){default_prompt}: ").strip() + if not choice: + if default_name: + for ip in adapter_ips.get(default_name, []): + if not ip.startswith("169.254"): + print(f"Using default network interface: {default_name} ({ip})") + return default_name, ip + ip = adapter_ips.get(default_name, [""])[0] + print(f"Using default network interface: {default_name} ({ip})") + return default_name, ip + else: + print("No default interface available. Please make a selection.") + continue + choice_idx = int(choice) - 1 + if 0 <= choice_idx < len(names_list): + selected_name = names_list[choice_idx] + ips = adapter_ips[selected_name] + selected_ip = "" + for ip in ips: + if not ip.startswith("169.254"): + selected_ip = ip + break + if not selected_ip and ips: + selected_ip = ips[0] + print(f"Using network interface: {selected_name} ({selected_ip})") + return selected_name, selected_ip + else: + print(f"Invalid selection. Please enter a number between 1 and {len(names_list)}.") + except ValueError: + print("Invalid input. Please enter a valid number.") + + +fake_tcp_injector = None +injector_thread = None + + +def run_injector_safe(w_filter, connections): + global fake_tcp_injector + try: + fake_tcp_injector = FakeTcpInjector(w_filter, connections) + fake_tcp_injector.run() + except Exception as e: + print(f"\n[Info] Injector stopped: {e}") + + +def stop_injector(): + global fake_tcp_injector + if fake_tcp_injector: + try: + fake_tcp_injector.w.close() + except Exception: + pass + fake_tcp_injector = None + + +def start_injector(ip: str): + global fake_tcp_injector, injector_thread + w_filter = ( + "tcp and " + f"((ip.SrcAddr == {ip} and ip.DstAddr == {CONNECT_IP} and tcp.DstPort == {CONNECT_PORT}) or " + f"(ip.SrcAddr == {CONNECT_IP} and ip.DstAddr == {ip} and tcp.SrcPort == {CONNECT_PORT})) and " + "(tcp.Syn or tcp.Rst or tcp.Fin or tcp.PayloadLength == 0)" + ) + print(f"\n[Info] Starting Fake TCP Injector with filter: {w_filter}") + injector_thread = threading.Thread( + target=run_injector_safe, + args=(w_filter, fake_injective_connections), + daemon=True + ) + injector_thread.start() + + +def monitor_adapter_loop(adapter_name: str, initial_ip: str): + global INTERFACE_IPV4 + last_ip = initial_ip + + if last_ip: + start_injector(last_ip) + else: + print(f"\n[Warning] Adapter '{adapter_name}' is currently disconnected. Waiting for it to connect...") + + while True: + time.sleep(2) + current_ip = "" + try: + adapters = get_adapters() + for a in adapters: + if a.get('InterfaceAlias') == adapter_name: + ip = a.get('IPAddress') + if ip and not ip.startswith("169.254"): + current_ip = ip + break + except Exception: + pass + + if last_ip and not current_ip: + print(f"\n[Warning] Adapter '{adapter_name}' disconnected! Pausing tunnel...") + stop_injector() + last_ip = "" + INTERFACE_IPV4 = "" + + elif current_ip and current_ip != last_ip: + if not last_ip: + print(f"\n[Info] Adapter '{adapter_name}' connected (IP: {current_ip}). Resuming tunnel...") + else: + print(f"\n[Info] Adapter '{adapter_name}' IP changed from {last_ip} to {current_ip}. Rebinding tunnel...") + stop_injector() + + INTERFACE_IPV4 = current_ip + last_ip = current_ip + start_injector(current_ip) + + if __name__ == "__main__": - w_filter = "tcp and " + "(" + "(ip.SrcAddr == " + INTERFACE_IPV4 + " and ip.DstAddr == " + CONNECT_IP + ")" + " or " + "(ip.SrcAddr == " + CONNECT_IP + " and ip.DstAddr == " + INTERFACE_IPV4 + ")" + ")" - fake_tcp_injector = FakeTcpInjector(w_filter, fake_injective_connections) - threading.Thread(target=fake_tcp_injector.run, args=(), daemon=True).start() + if not is_admin(): + print("This application requires administrator privileges. Attempting to elevate...") + run_as_admin() + + check_windivert_binaries() + + INTERFACE_NAME, INTERFACE_IPV4 = select_network_interface() + + # Start the adapter monitoring and rebinding loop in a daemon thread + threading.Thread( + target=monitor_adapter_loop, + args=(INTERFACE_NAME, INTERFACE_IPV4), + daemon=True + ).start() + print("هشن شومافر تیامح دینکیم هدافتسا دازآ تنرتنیا هب یسرتسد یارب همانرب نیا زا رگا") print( "دراد امش تیامح هب زاین هک مراد رظن رد دازآ تنرتنیا هب ناریا مدرم مامت یسرتسد یارب یدایز یاه همانرب و اه هژورپ") print("\n") print("USDT (BEP20): 0x76a768B53Ca77B43086946315f0BDF21156bF424\n") print("@patterniha") - asyncio.run(main()) + + try: + asyncio.run(main()) + except KeyboardInterrupt: + print("\n[Info] KeyboardInterrupt received. Stopping services gracefully...") + stop_injector() + print("[Info] Goodbye!") + sys.exit(0)