From 0cee44974cb869de8cf4f5a23d235e1903e07bab Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Fri, 29 May 2026 17:18:34 +0330 Subject: [PATCH 1/6] fix: improve logging and network resilience - Write most logs to log files instead of printing them to the console - Improve network error handling to prevent crashes during connectivity interruptions --- fake_tcp.py | 404 ++++++++++++++++++--------------- injecter.py | 92 +++++--- main.py | 639 +++++++++++++++++++++++++++++++++------------------- 3 files changed, 693 insertions(+), 442 deletions(-) diff --git a/fake_tcp.py b/fake_tcp.py index 8baf26c..0cdad2f 100644 --- a/fake_tcp.py +++ b/fake_tcp.py @@ -1,177 +1,227 @@ -import asyncio -import socket -import sys -import threading -import time - -from pydivert import Packet - -from monitor_connection import MonitorConnection -from injecter import TcpInjector - - -class FakeInjectiveConnection(MonitorConnection): - def __init__(self, sock: socket.socket, src_ip, dst_ip, - src_port, dst_port, fake_data: bytes, bypass_method: str, peer_sock: socket.socket): - super().__init__(sock, src_ip, dst_ip, src_port, dst_port) - self.fake_data = fake_data - self.sch_fake_sent = False - self.fake_sent = False - self.t2a_event = asyncio.Event() - self.t2a_msg = "" - self.bypass_method = bypass_method - self.peer_sock = peer_sock - self.running_loop = asyncio.get_running_loop() - - -class FakeTcpInjector(TcpInjector): - - def __init__(self, w_filter: str, connections: dict[tuple, FakeInjectiveConnection]): - super().__init__(w_filter) - self.connections = connections - - def fake_send_thread(self, packet: Packet, connection: FakeInjectiveConnection): - time.sleep(0.001) - with connection.thread_lock: - if not connection.monitor: - return - - packet.tcp.psh = True - packet.ip.packet_len = packet.ip.packet_len + len(connection.fake_data) - packet.tcp.payload = connection.fake_data - if packet.ipv4: - packet.ipv4.ident = (packet.ipv4.ident + 1) & 0xffff - # if connection.bypass_method == "wrong_checksum": - # ... - if connection.bypass_method == "wrong_seq": - packet.tcp.seq_num = (connection.syn_seq + 1 - len(packet.tcp.payload)) & 0xffffffff - connection.fake_sent = True - self.w.send(packet, True) - - - - - else: - sys.exit("not implemented method!") - - def on_unexpected_packet(self, packet: Packet, connection: FakeInjectiveConnection, info_m: str): - print(info_m, packet) - connection.sock.close() - connection.peer_sock.close() - connection.monitor = False - connection.t2a_msg = "unexpected_close" - connection.running_loop.call_soon_threadsafe(connection.t2a_event.set, ) - self.w.send(packet, False) - - def on_inbound_packet(self, packet: Packet, connection: FakeInjectiveConnection): - if connection.syn_seq == -1: - self.on_unexpected_packet(packet, connection, "unexpected inbound packet, no syn sent!") - return - if packet.tcp.ack and packet.tcp.syn and (not packet.tcp.rst) and (not packet.tcp.fin) and ( - len(packet.tcp.payload) == 0): - seq_num = packet.tcp.seq_num - ack_num = packet.tcp.ack_num - if connection.syn_ack_seq != -1 and connection.syn_ack_seq != seq_num: - self.on_unexpected_packet(packet, connection, - "unexpected inbound syn-ack packet, seq change! " + str(seq_num) + " " + str( - connection.syn_ack_seq)) - return - if ack_num != ((connection.syn_seq + 1) & 0xffffffff): - self.on_unexpected_packet(packet, connection, - "unexpected inbound syn-ack packet, ack not matched! " + str( - ack_num) + " " + str(connection.syn_seq)) - return - connection.syn_ack_seq = seq_num - self.w.send(packet, False) - return - if packet.tcp.ack and (not packet.tcp.syn) and (not packet.tcp.rst) and ( - not packet.tcp.fin) and (len(packet.tcp.payload) == 0) and connection.fake_sent: - seq_num = packet.tcp.seq_num - ack_num = packet.tcp.ack_num - if connection.syn_ack_seq == -1 or ((connection.syn_ack_seq + 1) & 0xffffffff) != seq_num: - self.on_unexpected_packet(packet, connection, - "unexpected inbound ack packet, seq not matched! " + str(seq_num) + " " + str( - connection.syn_ack_seq)) - return - if ack_num != ((connection.syn_seq + 1) & 0xffffffff): - self.on_unexpected_packet(packet, connection, - "unexpected inbound ack packet, ack not matched! " + str(ack_num) + " " + str( - connection.syn_seq)) - return - - connection.monitor = False - connection.t2a_msg = "fake_data_ack_recv" - connection.running_loop.call_soon_threadsafe(connection.t2a_event.set, ) - return - self.on_unexpected_packet(packet, connection, "unexpected inbound packet") - return - - def on_outbound_packet(self, packet: Packet, connection: FakeInjectiveConnection): - if connection.sch_fake_sent: - self.on_unexpected_packet(packet, connection, "unexpected outbound packet, recv packet after fake sent!") - return - if packet.tcp.syn and (not packet.tcp.ack) and (not packet.tcp.rst) and (not packet.tcp.fin) and ( - len(packet.tcp.payload) == 0): - seq_num = packet.tcp.seq_num - ack_num = packet.tcp.ack_num - if ack_num != 0: - self.on_unexpected_packet(packet, connection, "unexpected outbound syn packet, ack_num is not zero!") - return - if connection.syn_seq != -1 and connection.syn_seq != seq_num: - self.on_unexpected_packet(packet, connection, "unexpected outbound syn packet, seq not matched! " + str( - seq_num) + " " + str(connection.syn_seq)) - return - connection.syn_seq = seq_num - self.w.send(packet, False) - return - if packet.tcp.ack and (not packet.tcp.syn) and (not packet.tcp.rst) and (not packet.tcp.fin) and ( - len(packet.tcp.payload) == 0): - seq_num = packet.tcp.seq_num - ack_num = packet.tcp.ack_num - if connection.syn_seq == -1 or ((connection.syn_seq + 1) & 0xffffffff) != seq_num: - self.on_unexpected_packet(packet, connection, - "unexpected outbound ack packet, seq not matched! " + str( - seq_num) + " " + str( - connection.syn_seq)) - return - if connection.syn_ack_seq == -1 or ack_num != ((connection.syn_ack_seq + 1) & 0xffffffff): - self.on_unexpected_packet(packet, connection, - "unexpected outbound ack packet, ack not matched! " + str( - ack_num) + " " + str( - connection.syn_ack_seq)) - return - - self.w.send(packet, False) - connection.sch_fake_sent = True - threading.Thread(target=self.fake_send_thread, args=(packet, connection), daemon=True).start() - return - self.on_unexpected_packet(packet, connection, "unexpected outbound packet") - return - - def inject(self, packet: Packet): - if packet.is_inbound: - c_id = (packet.ip.dst_addr, packet.tcp.dst_port, packet.ip.src_addr, packet.tcp.src_port) - try: - connection = self.connections[c_id] - except KeyError: - self.w.send(packet, False) - else: - with connection.thread_lock: - if not connection.monitor: - self.w.send(packet, False) - return - self.on_inbound_packet(packet, connection) - elif packet.is_outbound: - c_id = (packet.ip.src_addr, packet.tcp.src_port, packet.ip.dst_addr, packet.tcp.dst_port) - try: - connection = self.connections[c_id] - except KeyError: - self.w.send(packet, False) - else: - with connection.thread_lock: - if not connection.monitor: - self.w.send(packet, False) - return - self.on_outbound_packet(packet, connection) - else: - sys.exit("impossible direction!") +import asyncio +import logging +import socket +import threading +import time + +from pydivert import Packet + +from monitor_connection import MonitorConnection +from injecter import TcpInjector + + +class FakeInjectiveConnection(MonitorConnection): + def __init__(self, sock: socket.socket, src_ip, dst_ip, + src_port, dst_port, fake_data: bytes, bypass_method: str, peer_sock: socket.socket): + super().__init__(sock, src_ip, dst_ip, src_port, dst_port) + self.fake_data = fake_data + self.sch_fake_sent = False + self.fake_sent = False + self.t2a_event = asyncio.Event() + self.t2a_msg = "" + self.bypass_method = bypass_method + self.peer_sock = peer_sock + self.running_loop = asyncio.get_running_loop() + + +class FakeTcpInjector(TcpInjector): + + def __init__(self, w_filter: str, connections: dict[tuple, FakeInjectiveConnection]): + super().__init__(w_filter) + self.connections = connections + + def _safe_send(self, packet: Packet, recalculate_checksum: bool): + if self.w is None: + logging.error("WinDivert handle is not ready; dropping packet send.") + return + try: + self.w.send(packet, recalculate_checksum) + except Exception: + logging.exception("Failed to send packet via WinDivert.") + + @staticmethod + def _safe_close_connection_sockets(connection: FakeInjectiveConnection): + try: + connection.sock.close() + except OSError: + pass + try: + connection.peer_sock.close() + except OSError: + pass + + @staticmethod + def _signal_unexpected_close(connection: FakeInjectiveConnection): + connection.monitor = False + connection.t2a_msg = "unexpected_close" + try: + connection.running_loop.call_soon_threadsafe(connection.t2a_event.set, ) + except RuntimeError: + # Event loop may already be closed during shutdown. + pass + + @staticmethod + def _is_pure_ack(packet: Packet) -> bool: + return ( + packet.tcp.ack + and (not packet.tcp.syn) + and (not packet.tcp.rst) + and (not packet.tcp.fin) + and (len(packet.tcp.payload) == 0) + ) + + @staticmethod + def _is_handshake_ack(packet: Packet, connection: FakeInjectiveConnection) -> bool: + expected_seq = (connection.syn_seq + 1) & 0xffffffff + expected_ack = (connection.syn_ack_seq + 1) & 0xffffffff + return packet.tcp.seq_num == expected_seq and packet.tcp.ack_num == expected_ack + + def fake_send_thread(self, packet: Packet, connection: FakeInjectiveConnection): + try: + time.sleep(0.001) + with connection.thread_lock: + if not connection.monitor: + return + + packet.tcp.psh = True + packet.ip.packet_len = packet.ip.packet_len + len(connection.fake_data) + packet.tcp.payload = connection.fake_data + if packet.ipv4: + packet.ipv4.ident = (packet.ipv4.ident + 1) & 0xffff + # if connection.bypass_method == "wrong_checksum": + # ... + if connection.bypass_method == "wrong_seq": + packet.tcp.seq_num = (connection.syn_seq + 1 - len(packet.tcp.payload)) & 0xffffffff + connection.fake_sent = True + self._safe_send(packet, True) + else: + self._signal_unexpected_close(connection) + logging.error("Unknown bypass method in fake_send_thread: %s", connection.bypass_method) + except Exception: + logging.exception("Unexpected error in fake_send_thread.") + + def on_unexpected_packet(self, packet: Packet, connection: FakeInjectiveConnection, info_m: str): + logging.warning("%s | packet=%s", info_m, packet) + self._safe_close_connection_sockets(connection) + self._signal_unexpected_close(connection) + self._safe_send(packet, False) + + def on_inbound_packet(self, packet: Packet, connection: FakeInjectiveConnection): + if connection.syn_seq == -1: + self.on_unexpected_packet(packet, connection, "unexpected inbound packet, no syn sent!") + return + if packet.tcp.ack and packet.tcp.syn and (not packet.tcp.rst) and (not packet.tcp.fin) and ( + len(packet.tcp.payload) == 0): + seq_num = packet.tcp.seq_num + ack_num = packet.tcp.ack_num + if connection.syn_ack_seq != -1 and connection.syn_ack_seq != seq_num: + self.on_unexpected_packet(packet, connection, + "unexpected inbound syn-ack packet, seq change! " + str(seq_num) + " " + str( + connection.syn_ack_seq)) + return + if ack_num != ((connection.syn_seq + 1) & 0xffffffff): + self.on_unexpected_packet(packet, connection, + "unexpected inbound syn-ack packet, ack not matched! " + str( + ack_num) + " " + str(connection.syn_seq)) + return + connection.syn_ack_seq = seq_num + self._safe_send(packet, False) + return + if packet.tcp.ack and (not packet.tcp.syn) and (not packet.tcp.rst) and ( + not packet.tcp.fin) and (len(packet.tcp.payload) == 0) and connection.fake_sent: + seq_num = packet.tcp.seq_num + ack_num = packet.tcp.ack_num + if connection.syn_ack_seq == -1 or ((connection.syn_ack_seq + 1) & 0xffffffff) != seq_num: + self.on_unexpected_packet(packet, connection, + "unexpected inbound ack packet, seq not matched! " + str(seq_num) + " " + str( + connection.syn_ack_seq)) + return + if ack_num != ((connection.syn_seq + 1) & 0xffffffff): + self.on_unexpected_packet(packet, connection, + "unexpected inbound ack packet, ack not matched! " + str(ack_num) + " " + str( + connection.syn_seq)) + return + + connection.monitor = False + connection.t2a_msg = "fake_data_ack_recv" + connection.running_loop.call_soon_threadsafe(connection.t2a_event.set, ) + return + self.on_unexpected_packet(packet, connection, "unexpected inbound packet") + return + + def on_outbound_packet(self, packet: Packet, connection: FakeInjectiveConnection): + if connection.sch_fake_sent: + if ( + connection.syn_seq != -1 + and connection.syn_ack_seq != -1 + and self._is_pure_ack(packet) + and self._is_handshake_ack(packet, connection) + ): + self._safe_send(packet, False) + return + self.on_unexpected_packet(packet, connection, "unexpected outbound packet, recv packet after fake sent!") + return + if packet.tcp.syn and (not packet.tcp.ack) and (not packet.tcp.rst) and (not packet.tcp.fin) and ( + len(packet.tcp.payload) == 0): + seq_num = packet.tcp.seq_num + ack_num = packet.tcp.ack_num + if ack_num != 0: + self.on_unexpected_packet(packet, connection, "unexpected outbound syn packet, ack_num is not zero!") + return + if connection.syn_seq != -1 and connection.syn_seq != seq_num: + self.on_unexpected_packet(packet, connection, "unexpected outbound syn packet, seq not matched! " + str( + seq_num) + " " + str(connection.syn_seq)) + return + connection.syn_seq = seq_num + self._safe_send(packet, False) + return + if self._is_pure_ack(packet): + seq_num = packet.tcp.seq_num + ack_num = packet.tcp.ack_num + if connection.syn_seq == -1 or ((connection.syn_seq + 1) & 0xffffffff) != seq_num: + self.on_unexpected_packet(packet, connection, + "unexpected outbound ack packet, seq not matched! " + str( + seq_num) + " " + str( + connection.syn_seq)) + return + if connection.syn_ack_seq == -1 or ack_num != ((connection.syn_ack_seq + 1) & 0xffffffff): + self.on_unexpected_packet(packet, connection, + "unexpected outbound ack packet, ack not matched! " + str( + ack_num) + " " + str( + connection.syn_ack_seq)) + return + + self._safe_send(packet, False) + connection.sch_fake_sent = True + threading.Thread(target=self.fake_send_thread, args=(packet, connection), daemon=True).start() + return + self.on_unexpected_packet(packet, connection, "unexpected outbound packet") + return + + def inject(self, packet: Packet): + if packet.is_inbound: + c_id = (packet.ip.dst_addr, packet.tcp.dst_port, packet.ip.src_addr, packet.tcp.src_port) + try: + connection = self.connections[c_id] + except KeyError: + self._safe_send(packet, False) + else: + with connection.thread_lock: + if not connection.monitor: + self._safe_send(packet, False) + return + self.on_inbound_packet(packet, connection) + elif packet.is_outbound: + c_id = (packet.ip.src_addr, packet.tcp.src_port, packet.ip.dst_addr, packet.tcp.dst_port) + try: + connection = self.connections[c_id] + except KeyError: + self._safe_send(packet, False) + else: + with connection.thread_lock: + if not connection.monitor: + self._safe_send(packet, False) + return + self.on_outbound_packet(packet, connection) + else: + logging.error("Unknown packet direction encountered; packet=%s", packet) diff --git a/injecter.py b/injecter.py index 8429b57..e491093 100644 --- a/injecter.py +++ b/injecter.py @@ -1,37 +1,55 @@ -import sys -from abc import ABC, abstractmethod - -from pydivert import WinDivert, Packet - - -# from pydivert.consts import * - - -class TcpInjector(ABC): - def __init__(self, w_filter: str): - # self.interface_ipv4 = interface_ipv4 - # self.interface_ipv6 = interface_ipv6 - # ip_filter = ip4_filter = ip6_filter = "" - # if self.interface_ipv4: - # ip4_filter = "(ip.SrcAddr == " + self.interface_ipv4 + " or ip.DstAddr == " + self.interface_ipv4 + ")" - # ip_filter = ip4_filter - # if self.interface_ipv6: - # ip6_filter = "(ipv6.SrcAddr == " + self.interface_ipv6 + " or ipv6.DstAddr == " + self.interface_ipv6 + ")" - # ip_filter = ip6_filter - # if self.interface_ipv4 and self.interface_ipv6: - # ip_filter = "(" + ip4_filter + " or " + ip6_filter + ")" - # - # self.filter = "tcp" - # if ip_filter: - # self.filter += " and " + ip_filter - self.w: WinDivert = WinDivert(w_filter) - - @abstractmethod - def inject(self, packet: Packet): - sys.exit("Not implemented") - - def run(self): - with self.w: - while True: - packet = self.w.recv(65575) - self.inject(packet) +import logging +import time +from abc import ABC, abstractmethod + +from pydivert import WinDivert, Packet + + +# from pydivert.consts import * + + +class TcpInjector(ABC): + def __init__(self, w_filter: str): + self.w_filter = w_filter + self.w: WinDivert | None = None + # self.interface_ipv4 = interface_ipv4 + # self.interface_ipv6 = interface_ipv6 + # ip_filter = ip4_filter = ip6_filter = "" + # if self.interface_ipv4: + # ip4_filter = "(ip.SrcAddr == " + self.interface_ipv4 + " or ip.DstAddr == " + self.interface_ipv4 + ")" + # ip_filter = ip4_filter + # if self.interface_ipv6: + # ip6_filter = "(ipv6.SrcAddr == " + self.interface_ipv6 + " or ipv6.DstAddr == " + self.interface_ipv6 + ")" + # ip_filter = ip6_filter + # if self.interface_ipv4 and self.interface_ipv6: + # ip_filter = "(" + ip4_filter + " or " + ip6_filter + ")" + # + # self.filter = "tcp" + # if ip_filter: + # self.filter += " and " + ip_filter + @abstractmethod + def inject(self, packet: Packet): + raise NotImplementedError("inject() must be implemented by subclasses") + + def run(self): + retry_delay_sec = 1 + while True: + try: + self.w = WinDivert(self.w_filter) + with self.w: + logging.info("WinDivert started with filter: %s", self.w_filter) + retry_delay_sec = 1 + while True: + try: + packet = self.w.recv(65575) + self.inject(packet) + except Exception: + logging.exception("Packet processing error in WinDivert loop.") + time.sleep(0.01) + except Exception: + logging.exception( + "WinDivert loop failed. Retrying in %s second(s).", + retry_delay_sec, + ) + time.sleep(retry_delay_sec) + retry_delay_sec = min(retry_delay_sec * 2, 10) diff --git a/main.py b/main.py index 3170b25..0345b99 100644 --- a/main.py +++ b/main.py @@ -1,228 +1,411 @@ -import asyncio -import os -import socket -import sys -import traceback -import threading -import json - -# from utils.proxy_protocols import parse_vless_protocol -from utils.network_tools import get_default_interface_ipv4 -from utils.packet_templates import ClientHelloMaker -from fake_tcp import FakeInjectiveConnection, FakeTcpInjector - - -def get_exe_dir(): - """Returns the directory where the .exe (or script) is located.""" - if getattr(sys, 'frozen', False): - # Running as a PyInstaller EXE - return os.path.dirname(sys.executable) - else: - # Running as a normal Python script - return os.path.dirname(os.path.abspath(__file__)) - - -# Build the path to config.json -config_path = os.path.join(get_exe_dir(), 'config.json') - -# 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"] -INTERFACE_IPV4 = get_default_interface_ipv4(CONNECT_IP) -DATA_MODE = "tls" -BYPASS_METHOD = "wrong_seq" - -################## - -fake_injective_connections: dict[tuple, FakeInjectiveConnection] = {} - - -async def relay_main_loop(sock_1: socket.socket, sock_2: socket.socket, peer_task: asyncio.Task, - first_prefix_data: bytes): - try: - loop = asyncio.get_running_loop() - while True: - try: - data = await loop.sock_recv(sock_1, 65575) - if not data: - raise ValueError("eof") - 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 - except Exception: - traceback.print_exc() - sys.exit("relay main loop error!") - - -async def handle(incoming_sock: socket.socket, incoming_remote_addr): - try: - loop = asyncio.get_running_loop() - # try: - # data = await loop.sock_recv(incoming_sock, 65575) - # if not data: - # raise ValueError("eof") - # except Exception: - # incoming_sock.close() - # return - # try: - # version, uuid_bytes, transport_protocol, remote_address_type, remote_address, remote_port, payload_index = parse_vless_protocol( - # data) - # except Exception as e: - # print("No Vless Request!, Connection Closed", repr(e), data) - # incoming_sock.close() - # return - # if transport_protocol != "tcp": - # print("Transport Protocol Error!, Connection Closed", transport_protocol, data) - # incoming_sock.close() - # return - # if remote_address_type == "hostname": - # print("hostname address not implemented yet!", data) - # incoming_sock.close() - # return - # if remote_address_type == "ipv4": - # if not INTERFACE_IPV4: - # print("no interface ipv4!", data) - # incoming_sock.close() - # return - # family = socket.AF_INET - # src_ip = INTERFACE_IPV4 - # - # elif remote_address_type == "ipv6": - # if not INTERFACE_IPV6: - # print("no interface ipv6!", data) - # incoming_sock.close() - # return - # family = socket.AF_INET6 - # src_ip = INTERFACE_IPV6 - # - # 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": - fake_data = ClientHelloMaker.get_client_hello_with(os.urandom(32), os.urandom(32), FAKE_SNI, - os.urandom(32)) - else: - sys.exit("impossible mode!") - outgoing_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - outgoing_sock.setblocking(False) - outgoing_sock.bind((INTERFACE_IPV4, 0)) - outgoing_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) - outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) - outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) - src_port = outgoing_sock.getsockname()[1] - fake_injective_conn = FakeInjectiveConnection(outgoing_sock, INTERFACE_IPV4, CONNECT_IP, src_port, CONNECT_PORT, - fake_data, - 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!") - 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 - - 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") - - -async def main(): - mother_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - mother_sock.setblocking(False) - mother_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - mother_sock.bind((LISTEN_HOST, LISTEN_PORT)) - mother_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) - mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) - mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) - mother_sock.listen() - loop = asyncio.get_running_loop() - while True: - incoming_sock, addr = await loop.sock_accept(mother_sock) - incoming_sock.setblocking(False) - incoming_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) - incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) - incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) - incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) - asyncio.create_task(handle(incoming_sock, addr)) - - -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() - print("هشن شومافر تیامح دینکیم هدافتسا دازآ تنرتنیا هب یسرتسد یارب همانرب نیا زا رگا") - print( - "دراد امش تیامح هب زاین هک مراد رظن رد دازآ تنرتنیا هب ناریا مدرم مامت یسرتسد یارب یدایز یاه همانرب و اه هژورپ") - print("\n") - print("USDT (BEP20): 0x76a768B53Ca77B43086946315f0BDF21156bF424\n") - print("@patterniha") - asyncio.run(main()) +import asyncio +import ctypes +import datetime +import logging +import os +import socket +import subprocess +import sys +import threading +import json + +# from utils.proxy_protocols import parse_vless_protocol +from utils.network_tools import get_default_interface_ipv4 +from utils.packet_templates import ClientHelloMaker +from fake_tcp import FakeInjectiveConnection, FakeTcpInjector + + +def get_exe_dir(): + """Returns the directory where the .exe (or script) is located.""" + if getattr(sys, 'frozen', False): + # Running as a PyInstaller EXE + return os.path.dirname(sys.executable) + else: + # Running as a normal Python script + return os.path.dirname(os.path.abspath(__file__)) + + +class DailyDateFileHandler(logging.FileHandler): + def __init__(self, logs_dir: str, base_name: str, encoding: str = "utf-8"): + self.logs_dir = logs_dir + self.base_name = base_name + self.current_date = self._today() + os.makedirs(self.logs_dir, exist_ok=True) + super().__init__(self._build_path(self.current_date), mode="a", encoding=encoding, delay=False) + + @staticmethod + def _today() -> str: + return datetime.date.today().strftime("%Y-%m-%d") + + def _build_path(self, date_text: str) -> str: + return os.path.join(self.logs_dir, f"{self.base_name}-{date_text}.log") + + def _roll_if_needed(self): + new_date = self._today() + if new_date == self.current_date: + return + + self.acquire() + try: + if new_date != self.current_date: + if self.stream: + self.stream.close() + self.stream = None + self.current_date = new_date + self.baseFilename = os.path.abspath(self._build_path(new_date)) + self.stream = self._open() + finally: + self.release() + + def emit(self, record: logging.LogRecord): + self._roll_if_needed() + super().emit(record) + + +def setup_logging(): + logs_dir = os.path.join(get_exe_dir(), "logs") + os.makedirs(logs_dir, exist_ok=True) + + formatter = logging.Formatter("%(asctime)s | %(levelname)s | %(message)s") + + file_handler = DailyDateFileHandler(logs_dir=logs_dir, base_name="sni-spoofing", encoding="utf-8") + file_handler.setFormatter(formatter) + + console_handler = logging.StreamHandler(sys.stdout) + console_handler.setFormatter(formatter) + + logging.basicConfig( + level=logging.INFO, + handlers=[file_handler, console_handler], + force=True, + ) + + +def setup_global_exception_logging(): + def sys_excepthook(exc_type, exc_value, exc_traceback): + if issubclass(exc_type, KeyboardInterrupt): + logging.info("KeyboardInterrupt received.") + return + logging.critical("Unhandled exception in main thread.", exc_info=(exc_type, exc_value, exc_traceback)) + + def thread_excepthook(args: threading.ExceptHookArgs): + if issubclass(args.exc_type, KeyboardInterrupt): + logging.info("KeyboardInterrupt received in thread: %s", args.thread.name) + return + logging.critical( + "Unhandled exception in thread: %s", + args.thread.name, + exc_info=(args.exc_type, args.exc_value, args.exc_traceback), + ) + + def unraisablehook(unraisable: sys.UnraisableHookArgs): + logging.critical( + "Unraisable exception: %s", + getattr(unraisable, "err_msg", "no message"), + exc_info=( + type(unraisable.exc_value), + unraisable.exc_value, + unraisable.exc_traceback, + ), + ) + + sys.excepthook = sys_excepthook + threading.excepthook = thread_excepthook + sys.unraisablehook = unraisablehook + + +def setup_asyncio_exception_logging(loop: asyncio.AbstractEventLoop): + def loop_exception_handler(_: asyncio.AbstractEventLoop, context: dict): + exc = context.get("exception") + msg = context.get("message", "Unhandled asyncio exception.") + if isinstance(exc, asyncio.CancelledError): + return + if exc is not None: + logging.error("Asyncio error: %s", msg, exc_info=(type(exc), exc, exc.__traceback__)) + else: + logging.error("Asyncio error: %s | context=%s", msg, context) + + loop.set_exception_handler(loop_exception_handler) + + +def is_running_as_admin() -> bool: + if os.name != "nt": + return True + try: + return bool(ctypes.windll.shell32.IsUserAnAdmin()) + except Exception: + return False + + +def relaunch_as_admin() -> bool: + if os.name != "nt": + return False + + if getattr(sys, "frozen", False): + executable = sys.executable + parameters = subprocess.list2cmdline(sys.argv[1:]) + else: + executable = sys.executable + script_path = os.path.abspath(__file__) + parameters = subprocess.list2cmdline([script_path, *sys.argv[1:]]) + + result = ctypes.windll.shell32.ShellExecuteW( + None, + "runas", + executable, + parameters, + None, + 1, + ) + return result > 32 + + +# Build the path to config.json +config_path = os.path.join(get_exe_dir(), 'config.json') + +# 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"] +DATA_MODE = "tls" +BYPASS_METHOD = "wrong_seq" + +################## + +fake_injective_connections: dict[tuple, FakeInjectiveConnection] = {} + + +def is_expected_disconnect(exc: Exception) -> bool: + if isinstance(exc, (ConnectionResetError, ConnectionAbortedError, BrokenPipeError)): + return True + if isinstance(exc, OSError): + return getattr(exc, "winerror", None) in {6, 64, 995, 10038, 10053, 10054, 10058} + return False + + +def safe_close_socket(sock: socket.socket | None): + if sock is None: + return + try: + sock.shutdown(socket.SHUT_RDWR) + except OSError: + pass + try: + sock.close() + except OSError: + pass + + +async def relay_main_loop(sock_1: socket.socket, sock_2: socket.socket, first_prefix_data: bytes): + loop = asyncio.get_running_loop() + try: + while True: + data = await loop.sock_recv(sock_1, 65575) + if not data: + break + if first_prefix_data: + data = first_prefix_data + data + first_prefix_data = b"" + await loop.sock_sendall(sock_2, data) + except asyncio.CancelledError: + raise + except Exception as exc: + if not is_expected_disconnect(exc): + logging.exception("Unexpected relay error: %r", exc) + + +async def handle(incoming_sock: socket.socket, incoming_remote_addr): + outgoing_sock: socket.socket | None = None + try: + loop = asyncio.get_running_loop() + interface_ipv4 = get_default_interface_ipv4(CONNECT_IP) + if not interface_ipv4: + logging.warning("No active IPv4 interface found for %s. Closing connection from %s.", CONNECT_IP, incoming_remote_addr) + return + # try: + # data = await loop.sock_recv(incoming_sock, 65575) + # if not data: + # raise ValueError("eof") + # except Exception: + # incoming_sock.close() + # return + # try: + # version, uuid_bytes, transport_protocol, remote_address_type, remote_address, remote_port, payload_index = parse_vless_protocol( + # data) + # except Exception as e: + # print("No Vless Request!, Connection Closed", repr(e), data) + # incoming_sock.close() + # return + # if transport_protocol != "tcp": + # print("Transport Protocol Error!, Connection Closed", transport_protocol, data) + # incoming_sock.close() + # return + # if remote_address_type == "hostname": + # print("hostname address not implemented yet!", data) + # incoming_sock.close() + # return + # if remote_address_type == "ipv4": + # if not INTERFACE_IPV4: + # print("no interface ipv4!", data) + # incoming_sock.close() + # return + # family = socket.AF_INET + # src_ip = INTERFACE_IPV4 + # + # elif remote_address_type == "ipv6": + # if not INTERFACE_IPV6: + # print("no interface ipv6!", data) + # incoming_sock.close() + # return + # family = socket.AF_INET6 + # src_ip = INTERFACE_IPV6 + # + # 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": + fake_data = ClientHelloMaker.get_client_hello_with(os.urandom(32), os.urandom(32), FAKE_SNI, + os.urandom(32)) + else: + logging.error("Invalid DATA_MODE: %s", DATA_MODE) + return + outgoing_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + outgoing_sock.setblocking(False) + outgoing_sock.bind((interface_ipv4, 0)) + outgoing_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) + outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) + outgoing_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) + src_port = outgoing_sock.getsockname()[1] + fake_injective_conn = FakeInjectiveConnection(outgoing_sock, interface_ipv4, CONNECT_IP, src_port, CONNECT_PORT, + fake_data, + 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: + logging.error("Unexpected t2a message: %s", fake_injective_conn.t2a_msg) + return + except Exception: + fake_injective_conn.monitor = False + del fake_injective_connections[fake_injective_conn.id] + outgoing_sock.close() + incoming_sock.close() + return + else: + logging.error("Unknown BYPASS_METHOD: %s", BYPASS_METHOD) + return + + 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 + + ito_task = asyncio.create_task(relay_main_loop(incoming_sock, outgoing_sock, b"")) + oti_task = asyncio.create_task(relay_main_loop(outgoing_sock, incoming_sock, b"")) # bytes([version, 0]) + _, pending = await asyncio.wait({ito_task, oti_task}, return_when=asyncio.FIRST_COMPLETED) + for task in pending: + task.cancel() + await asyncio.gather(*pending, return_exceptions=True) + + + + except Exception: + logging.exception("Unexpected error in connection handler from %s", incoming_remote_addr) + finally: + safe_close_socket(outgoing_sock) + safe_close_socket(incoming_sock) + + +async def main(): + mother_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + mother_sock.setblocking(False) + mother_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + mother_sock.bind((LISTEN_HOST, LISTEN_PORT)) + mother_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) + mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) + mother_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) + mother_sock.listen() + loop = asyncio.get_running_loop() + setup_asyncio_exception_logging(loop) + try: + while True: + incoming_sock, addr = await loop.sock_accept(mother_sock) + incoming_sock.setblocking(False) + incoming_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) + incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPIDLE, 11) + incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, 2) + incoming_sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_KEEPCNT, 3) + asyncio.create_task(handle(incoming_sock, addr)) + finally: + safe_close_socket(mother_sock) + + +if __name__ == "__main__": + setup_logging() + setup_global_exception_logging() + + if not is_running_as_admin(): + logging.warning("Program is not running as administrator. Trying to relaunch with admin rights.") + if relaunch_as_admin(): + logging.info("Admin relaunch requested successfully. Exiting current process.") + sys.exit(0) + logging.error("Failed to relaunch as administrator.") + sys.exit(1) + + w_filter = "tcp and (ip.DstAddr == " + CONNECT_IP + " or ip.SrcAddr == " + CONNECT_IP + ")" + fake_tcp_injector = FakeTcpInjector(w_filter, fake_injective_connections) + threading.Thread(target=fake_tcp_injector.run, args=(), daemon=True).start() + logging.info("Program started.") + print("هشن شومافر تیامح دینکیم هدافتسا دازآ تنرتنیا هب یسرتسد یارب همانرب نیا زا رگا") + print( + "دراد امش تیامح هب زاین هک مراد رظن رد دازآ تنرتنیا هب ناریا مدرم مامت یسرتسد یارب یدایز یاه همانرب و اه هژورپ") + print("\n") + print("USDT (BEP20): 0x76a768B53Ca77B43086946315f0BDF21156bF424\n") + print("@patterniha") + try: + asyncio.run(main()) + except KeyboardInterrupt: + logging.info("Shutdown requested by user (Ctrl+C).") From 0135a6711b94512add7cf72656d037863b095b2c Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Sat, 6 Jun 2026 10:55:05 +0330 Subject: [PATCH 2/6] Revise README with project details and improvements Updated project description and features in README. --- README.md | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c79e626..757fdd1 100644 --- a/README.md +++ b/README.md @@ -1,15 +1,18 @@ # SNI-Spoofing -Bypass DPI with IP/TCP-Header manipulation -حمایت کنید کارهای بزرگی در دست انجام هست: +دور زدن DPI با دستکاری هدرهای IP و TCP -USDT (BEP20): 0x76a768B53Ca77B43086946315f0BDF21156bF424 +این پروژه از [patterniha/SNI-Spoofing](https://github.com/patterniha/SNI-Spoofing) فورک شده است. -USDT (TRC20): TU5gKvKqcXPn8itp1DouBCwcqGHMemBm8o +## تفاوت این پروژه با [patterniha/SNI-Spoofing](https://github.com/patterniha/SNI-Spoofing) +در این پروژه تغییرات زیر اعمال شده‌اند: +- بخش عمدهٔ لاگ‌ها به‌جای نمایش در کنسول، در فایل‌های لاگ داخل مسیر `/logs` ذخیره می‌شوند. +- فایل‌های لاگ بر اساس تاریخ تفکیک شده‌اند؛ به‌طوری که برای هر روز یک فایل لاگ جداگانه ایجاد می‌شود. +- در نسخهٔ اصلی، هنگام قطع شدن اتصال شبکه، برنامه دچار Crash می‌شد. این مشکل برطرف شده است. +- از آنجا که اجرای برنامه به دسترسی Administrator نیاز دارد، در ویندوز برنامه به‌صورت خودکار با سطح دسترسی Administrator اجرا می‌شود. +--- -https://t.me/projectXhttp - -https://t.me/patterniha +به امید روزی که همه به اینترنت آزاد و بدون محدودیت دسترسی داشته باشند. 🕊 From ee1089e1ee63475dec7c1ddab5c9ec93aa9f82a9 Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:35:19 +0330 Subject: [PATCH 3/6] Update main.py --- main.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/main.py b/main.py index 0345b99..e12c1e1 100644 --- a/main.py +++ b/main.py @@ -8,6 +8,7 @@ import sys import threading import json +from typing import Any # from utils.proxy_protocols import parse_vless_protocol from utils.network_tools import get_default_interface_ipv4 @@ -88,7 +89,7 @@ def sys_excepthook(exc_type, exc_value, exc_traceback): return logging.critical("Unhandled exception in main thread.", exc_info=(exc_type, exc_value, exc_traceback)) - def thread_excepthook(args: threading.ExceptHookArgs): + def thread_excepthook(args: Any): if issubclass(args.exc_type, KeyboardInterrupt): logging.info("KeyboardInterrupt received in thread: %s", args.thread.name) return @@ -98,7 +99,7 @@ def thread_excepthook(args: threading.ExceptHookArgs): exc_info=(args.exc_type, args.exc_value, args.exc_traceback), ) - def unraisablehook(unraisable: sys.UnraisableHookArgs): + def unraisablehook(unraisable: Any): logging.critical( "Unraisable exception: %s", getattr(unraisable, "err_msg", "no message"), @@ -399,12 +400,10 @@ async def main(): fake_tcp_injector = FakeTcpInjector(w_filter, fake_injective_connections) threading.Thread(target=fake_tcp_injector.run, args=(), daemon=True).start() logging.info("Program started.") - print("هشن شومافر تیامح دینکیم هدافتسا دازآ تنرتنیا هب یسرتسد یارب همانرب نیا زا رگا") - print( - "دراد امش تیامح هب زاین هک مراد رظن رد دازآ تنرتنیا هب ناریا مدرم مامت یسرتسد یارب یدایز یاه همانرب و اه هژورپ") print("\n") - print("USDT (BEP20): 0x76a768B53Ca77B43086946315f0BDF21156bF424\n") - print("@patterniha") + print("repo link: https://github.com/SAMPA-asa/sni-spoofing") + print("forked from: https://github.com/patterniha/SNI-Spoofing") + print("\n") try: asyncio.run(main()) except KeyboardInterrupt: From fd4471958a8f60a16db88d0c1ad09169074ba2c7 Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:45:20 +0330 Subject: [PATCH 4/6] Update README.md --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index 757fdd1..04dee49 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ - در نسخهٔ اصلی، هنگام قطع شدن اتصال شبکه، برنامه دچار Crash می‌شد. این مشکل برطرف شده است. - از آنجا که اجرای برنامه به دسترسی Administrator نیاز دارد، در ویندوز برنامه به‌صورت خودکار با سطح دسترسی Administrator اجرا می‌شود. +--- +لینک کانال تلگرام: https://t.me/Sampa_Asa +راه ارتباطی: https://t.me/Sampa_Asa?direct --- به امید روزی که همه به اینترنت آزاد و بدون محدودیت دسترسی داشته باشند. 🕊 From fa24a68fcc5387ba8441a782f977ac00bc98ee2f Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Sun, 7 Jun 2026 21:47:06 +0330 Subject: [PATCH 5/6] Fix Telegram links formatting in README Updated Telegram channel links with HTML line breaks. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 04dee49..06d7589 100644 --- a/README.md +++ b/README.md @@ -14,8 +14,8 @@ - از آنجا که اجرای برنامه به دسترسی Administrator نیاز دارد، در ویندوز برنامه به‌صورت خودکار با سطح دسترسی Administrator اجرا می‌شود. --- -لینک کانال تلگرام: https://t.me/Sampa_Asa -راه ارتباطی: https://t.me/Sampa_Asa?direct +- لینک کانال تلگرام: https://t.me/Sampa_Asa
+- راه ارتباطی: https://t.me/Sampa_Asa?direct --- به امید روزی که همه به اینترنت آزاد و بدون محدودیت دسترسی داشته باشند. 🕊 From 9e5b02db5a129fe108d06e510f28d8edf83a82cc Mon Sep 17 00:00:00 2001 From: ASA <281823881+SAMPA-ASA@users.noreply.github.com> Date: Sun, 7 Jun 2026 22:16:01 +0330 Subject: [PATCH 6/6] Update hope statement for true internet freedom --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 06d7589..6009d1f 100644 --- a/README.md +++ b/README.md @@ -18,4 +18,4 @@ - راه ارتباطی: https://t.me/Sampa_Asa?direct --- -به امید روزی که همه به اینترنت آزاد و بدون محدودیت دسترسی داشته باشند. 🕊 +به امید روزی که همه به اینترنت آزاد واقعی دست پیدا کنیم. 🕊