From fb75ba4ea93331befa61992388a617a3b95070b4 Mon Sep 17 00:00:00 2001 From: Abolfazl Date: Thu, 23 Apr 2026 13:29:50 +0330 Subject: [PATCH 1/5] Removed netifaces for better compatibility --- README.md | 2 +- README_FA.md | 1 - requirements.txt | 4 +- src/lan_utils.py | 161 ++++++++++++++++++++++++++--------------------- 4 files changed, 91 insertions(+), 77 deletions(-) diff --git a/README.md b/README.md index ceff8f3..5bd014f 100644 --- a/README.md +++ b/README.md @@ -278,7 +278,7 @@ Install everything from [`requirements.txt`](requirements.txt). All listed packa | `h2` | HTTP/2 multiplexing to the Apps Script relay (significantly faster) | | `brotli` | Decompression of `Content-Encoding: br` responses | | `zstandard` | Decompression of `Content-Encoding: zstd` responses | -| `netifaces` | Better network interface detection for LAN sharing (fallback available without it) | + ### Load Balancing diff --git a/README_FA.md b/README_FA.md index 5af49ad..c697090 100644 --- a/README_FA.md +++ b/README_FA.md @@ -226,7 +226,6 @@ json | `h2` | ارتباط HTTP/2 با رله Apps Script (به‌طور محسوسی سریع‌تر) | | `brotli` | پشتیبانی از فشرده‌سازی `Content-Encoding: br` | | `zstandard` | پشتیبانی از فشرده‌سازی `Content-Encoding: zstd` | -| `netifaces` | تشخیص بهتر اینترفیس‌های شبکه برای اشتراک‌گذاری LAN (در صورت نبود آن، حالت جایگزین در دسترس است) | ### استفاده از چند Script ID diff --git a/requirements.txt b/requirements.txt index c0f1980..ee2b572 100644 --- a/requirements.txt +++ b/requirements.txt @@ -13,5 +13,5 @@ brotli>=1.1.0 # Optional: Zstandard decompression (some CDNs now serve `zstd`) zstandard>=0.22.0 -# Optional: Better network interface detection for LAN sharing -netifaces>=0.11.0 +# LAN interface detection now uses only the Python standard library +# (works on Windows, Linux, macOS, Android/Termux without a C compiler). diff --git a/src/lan_utils.py b/src/lan_utils.py index b8e271d..ec6648b 100644 --- a/src/lan_utils.py +++ b/src/lan_utils.py @@ -3,92 +3,104 @@ LAN utilities for detecting network interfaces and IP addresses. Provides functionality to enumerate local network interfaces and their associated IP addresses for LAN proxy sharing. + +Implementation notes +-------------------- +This module intentionally relies only on the Python standard library so +that it works out-of-the-box on every supported OS (Windows, Linux, +macOS, Android/Termux, *BSD) without requiring a C compiler or native +build tools (previous versions depended on ``netifaces``, which needs +"Microsoft Visual C++ 14.0 or greater" on Windows and was a frequent +install blocker for users on slow connections). + +Strategy (in order): +1. "UDP connect trick" to reliably discover the primary outbound + IPv4 and IPv6 addresses on any OS. +2. ``socket.getaddrinfo(hostname, ...)`` to enumerate additional + addresses bound to the host (covers multi-homed machines). + +These two steps together cover every real-world LAN scenario on +Windows, Linux, macOS, Android/Termux and *BSD. (We intentionally do +*not* try to map per-interface names to IPs via the stdlib — that is +not portable and, on Windows, triggers 30 s DNS timeouts.) """ import ipaddress import logging import socket -from typing import Dict, List, Optional +from typing import Dict, List, Optional, Set log = logging.getLogger("LAN") +# --------------------------------------------------------------------------- +# Primary-IP discovery (UDP connect trick) +# --------------------------------------------------------------------------- +def _primary_ip(family: int, probe_addr: str) -> Optional[str]: + """ + Return the primary local IP the OS would use to reach ``probe_addr``. + + Uses a connected UDP socket which does *not* actually send packets — + the kernel just picks the source address from its routing table. + Works identically on Windows, Linux, macOS, and Android. + """ + s = socket.socket(family, socket.SOCK_DGRAM) + try: + s.settimeout(0.5) + # Port 80 is arbitrary; no packet is sent for UDP connect(). + s.connect((probe_addr, 80)) + ip = s.getsockname()[0] + if family == socket.AF_INET6: + ip = ip.split('%', 1)[0] # strip zone id + return ip + except OSError: + return None + finally: + s.close() + + +# --------------------------------------------------------------------------- +# Public API +# --------------------------------------------------------------------------- def get_network_interfaces() -> Dict[str, List[str]]: """ - Get all network interfaces and their associated IP addresses. - - Returns a dictionary mapping interface names to lists of IP addresses - (both IPv4 and IPv6). Only includes interfaces with valid IP addresses - that are not loopback. + Get network interfaces and their associated non-loopback IP addresses. Returns: - Dict[str, List[str]]: Interface name -> list of IP addresses + Dict[str, List[str]]: Interface label -> list of IP addresses. + Interface names are best-effort; on some platforms we fall back + to synthetic labels such as ``"primary"`` / ``"primary_ipv6"``. """ - interfaces = {} + interfaces: Dict[str, List[str]] = {} + seen_ips: Set[str] = set() + def _add(label: str, ip: Optional[str]) -> None: + if not ip or ip in seen_ips: + return + if ip.startswith('127.') or ip == '::1': + return + seen_ips.add(ip) + interfaces.setdefault(label, []).append(ip) + + # 1) Primary outbound IPs (most reliable, cross-platform). + _add('primary', _primary_ip(socket.AF_INET, '192.0.2.1')) # TEST-NET-1 + _add('primary_ipv6', _primary_ip(socket.AF_INET6, '2001:db8::1')) # doc prefix + + # 2) Enumerate via hostname resolution (picks up multi-homed hosts). try: - import netifaces - for iface in netifaces.interfaces(): - addrs = netifaces.ifaddresses(iface) - ips = [] - # IPv4 addresses - if netifaces.AF_INET in addrs: - for addr in addrs[netifaces.AF_INET]: - ip = addr.get('addr') - if ip and not ip.startswith('127.'): - ips.append(ip) - # IPv6 addresses (without scope) - if netifaces.AF_INET6 in addrs: - for addr in addrs[netifaces.AF_INET6]: - ip = addr.get('addr') - if ip and not ip.startswith('::1') and not '%' in ip: - # Remove scope if present - ips.append(ip.split('%')[0]) - if ips: - interfaces[iface] = ips - except ImportError: - # Fallback to socket method for basic detection - log.debug("netifaces not available, using socket fallback") - interfaces = _get_interfaces_socket_fallback() - - return interfaces - - -def _get_interfaces_socket_fallback() -> Dict[str, List[str]]: - """ - Fallback method to get network interfaces using socket. - - This is less comprehensive than netifaces but works without extra dependencies. - """ - interfaces = {} - - try: - # Get hostname and try to resolve to IPs hostname = socket.gethostname() - try: - # Get IPv4 addresses - ipv4_info = socket.getaddrinfo(hostname, None, socket.AF_INET) - ipv4_addrs = [info[4][0] for info in ipv4_info if not info[4][0].startswith('127.')] - if ipv4_addrs: - interfaces['primary'] = list(set(ipv4_addrs)) # Remove duplicates - except socket.gaierror: - pass + except OSError: + hostname = '' + for family, label in ((socket.AF_INET, 'host'), (socket.AF_INET6, 'host_ipv6')): try: - # Get IPv6 addresses - ipv6_info = socket.getaddrinfo(hostname, None, socket.AF_INET6) - ipv6_addrs = [] - for info in ipv6_info: + for info in socket.getaddrinfo(hostname, None, family): ip = info[4][0] - if not ip.startswith('::1') and not '%' in ip: - ipv6_addrs.append(ip.split('%')[0]) - if ipv6_addrs: - interfaces['primary_ipv6'] = list(set(ipv6_addrs)) - except socket.gaierror: - pass - - except Exception as e: - log.debug("Socket fallback failed: %s", e) + if family == socket.AF_INET6: + ip = ip.split('%', 1)[0] + _add(label, ip) + except (socket.gaierror, OSError): + continue return interfaces @@ -107,21 +119,24 @@ def get_lan_ips(port: int = 8085) -> List[str]: List[str]: List of "IP:port" strings for LAN access """ interfaces = get_network_interfaces() - lan_addresses = [] + lan_addresses: List[str] = [] for iface_ips in interfaces.values(): for ip in iface_ips: try: - # Validate IP and check if it's a private address addr = ipaddress.ip_address(ip) - if addr.is_private or addr.is_link_local: - lan_addresses.append(f"{ip}:{port}") except ValueError: continue + if addr.is_loopback or addr.is_unspecified: + continue + # Include private, link-local, and unique-local (IPv6 fc00::/7) ranges. + if addr.is_private or addr.is_link_local: + bracket = f"[{ip}]" if isinstance(addr, ipaddress.IPv6Address) else ip + lan_addresses.append(f"{bracket}:{port}") - # Remove duplicates while preserving order - seen = set() - unique_addresses = [] + # Remove duplicates while preserving order. + seen: Set[str] = set() + unique_addresses: List[str] = [] for addr in lan_addresses: if addr not in seen: seen.add(addr) From 807ce60703853331f92d65d1e244709a77e38366 Mon Sep 17 00:00:00 2001 From: Amin Mahmoudi Date: Thu, 23 Apr 2026 13:31:12 +0330 Subject: [PATCH 2/5] Update README.md --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index 5bd014f..c780552 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,28 @@ For the latest news, releases, and project updates, follow our Telegram channel: --- +### If you like this project, please support it by starring it on GitHub (⭐). It helps the project get discovered. + +--- + +### Optional Financial Support 💸 + +- TON network: + +`masterking32.ton` + +- EVM-compatible networks (ETH and compatible chains): + +`0x517f07305D6ED781A089322B6cD93d1461bF8652` + +- TRC20 network (TRON): + +`TLApdY8APWkFHHoxebxGY8JhMeChiETqFH` + +Every contribution and every piece of feedback is appreciated. Support directly helps ongoing development and improvement. + +--- + ## Disclaimer MasterHttpRelayVPN is provided for educational, testing, and research purposes only. From 41f3be97ad04ed49d785dbfd61a0d5830880ceb7 Mon Sep 17 00:00:00 2001 From: Amin Mahmoudi Date: Thu, 23 Apr 2026 13:31:49 +0330 Subject: [PATCH 3/5] Update README_FA.md --- README_FA.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README_FA.md b/README_FA.md index c697090..c88cdcd 100644 --- a/README_FA.md +++ b/README_FA.md @@ -14,6 +14,28 @@ --- +### اگر از پروژه راضی‌اید، با دادن ستاره (⭐) در گیت‌هاب از ما حمایت کنید — این کار به دیده‌شدن پروژه کمک می‌کند. + +--- + +### حمایت مالی (اختیاری) 💸 + +- شبکه TON: + +`masterking32.ton` + +- آدرس روی شبکه‌های EVM (ETH و سازگارها): + +`0x517f07305D6ED781A089322B6cD93d1461bF8652` + +- شبکه TRC20 (TRON): + +`TLApdY8APWkFHHoxebxGY8JhMeChiETqFH` + +از هر نوع حمایت و بازخورد شما سپاسگزاریم — کمک‌ها برای توسعه و بهبود پروژه بسیار ارزشمند است. + +--- + ## سلب مسئولیت پروژه MasterHttpRelayVPN فقط برای اهداف آموزشی، تست و پژوهش ارائه شده است. From 8b2bfa35fce0d2bc6569779649efbc6d72e5327e Mon Sep 17 00:00:00 2001 From: Abolfazl Date: Thu, 23 Apr 2026 13:36:46 +0330 Subject: [PATCH 4/5] Refine LAN utilities documentation and restrict to IPv4 addresses --- src/lan_utils.py | 78 ++++++++++++++++++++---------------------------- 1 file changed, 33 insertions(+), 45 deletions(-) diff --git a/src/lan_utils.py b/src/lan_utils.py index ec6648b..6e88ea8 100644 --- a/src/lan_utils.py +++ b/src/lan_utils.py @@ -1,28 +1,25 @@ """ -LAN utilities for detecting network interfaces and IP addresses. +LAN utilities for detecting network interfaces and IPv4 addresses. -Provides functionality to enumerate local network interfaces and their -associated IP addresses for LAN proxy sharing. +Provides functionality to enumerate local IPv4 addresses for LAN proxy +sharing. IPv6 is intentionally not reported — this project only exposes +the proxy over IPv4 LANs, which is what every consumer router and +phone/desktop client actually uses. Implementation notes -------------------- -This module intentionally relies only on the Python standard library so -that it works out-of-the-box on every supported OS (Windows, Linux, -macOS, Android/Termux, *BSD) without requiring a C compiler or native -build tools (previous versions depended on ``netifaces``, which needs +This module relies only on the Python standard library so it works +out-of-the-box on every supported OS (Windows, Linux, macOS, +Android/Termux, *BSD) without requiring a C compiler or native build +tools (previous versions depended on ``netifaces``, which needs "Microsoft Visual C++ 14.0 or greater" on Windows and was a frequent install blocker for users on slow connections). Strategy (in order): 1. "UDP connect trick" to reliably discover the primary outbound - IPv4 and IPv6 addresses on any OS. -2. ``socket.getaddrinfo(hostname, ...)`` to enumerate additional - addresses bound to the host (covers multi-homed machines). - -These two steps together cover every real-world LAN scenario on -Windows, Linux, macOS, Android/Termux and *BSD. (We intentionally do -*not* try to map per-interface names to IPs via the stdlib — that is -not portable and, on Windows, triggers 30 s DNS timeouts.) + IPv4 address on any OS. +2. ``socket.getaddrinfo(hostname, AF_INET)`` to enumerate any additional + IPv4 addresses bound to the host (covers multi-homed machines). """ import ipaddress @@ -36,23 +33,20 @@ log = logging.getLogger("LAN") # --------------------------------------------------------------------------- # Primary-IP discovery (UDP connect trick) # --------------------------------------------------------------------------- -def _primary_ip(family: int, probe_addr: str) -> Optional[str]: +def _primary_ipv4() -> Optional[str]: """ - Return the primary local IP the OS would use to reach ``probe_addr``. + Return the primary local IPv4 the OS would use for outbound traffic. Uses a connected UDP socket which does *not* actually send packets — the kernel just picks the source address from its routing table. Works identically on Windows, Linux, macOS, and Android. """ - s = socket.socket(family, socket.SOCK_DGRAM) + s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) try: s.settimeout(0.5) - # Port 80 is arbitrary; no packet is sent for UDP connect(). - s.connect((probe_addr, 80)) - ip = s.getsockname()[0] - if family == socket.AF_INET6: - ip = ip.split('%', 1)[0] # strip zone id - return ip + # TEST-NET-1 address, port is arbitrary; no packet is sent for UDP connect(). + s.connect(('192.0.2.1', 80)) + return s.getsockname()[0] except OSError: return None finally: @@ -64,12 +58,12 @@ def _primary_ip(family: int, probe_addr: str) -> Optional[str]: # --------------------------------------------------------------------------- def get_network_interfaces() -> Dict[str, List[str]]: """ - Get network interfaces and their associated non-loopback IP addresses. + Get network interfaces and their associated non-loopback IPv4 addresses. Returns: - Dict[str, List[str]]: Interface label -> list of IP addresses. - Interface names are best-effort; on some platforms we fall back - to synthetic labels such as ``"primary"`` / ``"primary_ipv6"``. + Dict[str, List[str]]: Interface label -> list of IPv4 addresses. + Labels are best-effort synthetic names such as ``"primary"`` + and ``"host"``. """ interfaces: Dict[str, List[str]] = {} seen_ips: Set[str] = set() @@ -77,14 +71,13 @@ def get_network_interfaces() -> Dict[str, List[str]]: def _add(label: str, ip: Optional[str]) -> None: if not ip or ip in seen_ips: return - if ip.startswith('127.') or ip == '::1': + if ip.startswith('127.'): return seen_ips.add(ip) interfaces.setdefault(label, []).append(ip) - # 1) Primary outbound IPs (most reliable, cross-platform). - _add('primary', _primary_ip(socket.AF_INET, '192.0.2.1')) # TEST-NET-1 - _add('primary_ipv6', _primary_ip(socket.AF_INET6, '2001:db8::1')) # doc prefix + # 1) Primary outbound IPv4 (most reliable, cross-platform). + _add('primary', _primary_ipv4()) # 2) Enumerate via hostname resolution (picks up multi-homed hosts). try: @@ -92,22 +85,19 @@ def get_network_interfaces() -> Dict[str, List[str]]: except OSError: hostname = '' - for family, label in ((socket.AF_INET, 'host'), (socket.AF_INET6, 'host_ipv6')): + if hostname: try: - for info in socket.getaddrinfo(hostname, None, family): - ip = info[4][0] - if family == socket.AF_INET6: - ip = ip.split('%', 1)[0] - _add(label, ip) + for info in socket.getaddrinfo(hostname, None, socket.AF_INET): + _add('host', info[4][0]) except (socket.gaierror, OSError): - continue + pass return interfaces def get_lan_ips(port: int = 8085) -> List[str]: """ - Get list of LAN-accessible proxy addresses. + Get list of LAN-accessible proxy addresses (IPv4 only). Returns a list of IP:port combinations that can be used to access the proxy from other devices on the local network. @@ -124,15 +114,13 @@ def get_lan_ips(port: int = 8085) -> List[str]: for iface_ips in interfaces.values(): for ip in iface_ips: try: - addr = ipaddress.ip_address(ip) - except ValueError: + addr = ipaddress.IPv4Address(ip) + except (ValueError, ipaddress.AddressValueError): continue if addr.is_loopback or addr.is_unspecified: continue - # Include private, link-local, and unique-local (IPv6 fc00::/7) ranges. if addr.is_private or addr.is_link_local: - bracket = f"[{ip}]" if isinstance(addr, ipaddress.IPv6Address) else ip - lan_addresses.append(f"{bracket}:{port}") + lan_addresses.append(f"{ip}:{port}") # Remove duplicates while preserving order. seen: Set[str] = set() From 7b1812c4544ecd294eabf0418b1b328310eca43e Mon Sep 17 00:00:00 2001 From: Abolfazl Date: Thu, 23 Apr 2026 13:37:19 +0330 Subject: [PATCH 5/5] Enhance direct tunnel timeout handling for IP literals and improve logging for non-TLS traffic --- src/proxy_server.py | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/proxy_server.py b/src/proxy_server.py index 93416eb..46ca267 100644 --- a/src/proxy_server.py +++ b/src/proxy_server.py @@ -533,12 +533,17 @@ class ProxyServer: # • port 443 → MITM + relay through Apps Script # • port 80 → plain-HTTP relay through Apps Script # • other → give up (non-HTTP; can't be relayed) + # We use a shorter connect timeout for IP literals (4 s) because + # when the route is DPI-dropped, waiting longer doesn't help and + # clients like Telegram speed up DC-rotation when we fail fast. # We remember per-IP failures for a short while so subsequent # connects skip the doomed direct attempt. if _is_ip_literal(host): if not self._direct_temporarily_disabled(host): log.info("Direct tunnel → %s:%d (IP literal)", host, port) - ok = await self._do_direct_tunnel(host, port, reader, writer) + ok = await self._do_direct_tunnel( + host, port, reader, writer, timeout=4.0, + ) if ok: return self._remember_direct_failure(host, ttl=300) @@ -772,7 +777,8 @@ class ProxyServer: async def _do_direct_tunnel(self, host: str, port: int, reader: asyncio.StreamReader, writer: asyncio.StreamWriter, - connect_ip: str | None = None): + connect_ip: str | None = None, + timeout: float = 10.0): """Pipe raw TLS bytes directly to the target server. connect_ip overrides DNS: the TCP connection goes to that IP @@ -783,7 +789,7 @@ class ProxyServer: """ target_ip = connect_ip or host try: - r_remote, w_remote = await self._open_tcp_connection(target_ip, port, timeout=10) + r_remote, w_remote = await self._open_tcp_connection(target_ip, port, timeout=timeout) except Exception as e: log.error("Direct tunnel connect failed (%s via %s): %s", host, target_ip, e) @@ -914,17 +920,15 @@ class ProxyServer: # • Telegram Desktop / MTProto over port 443 sends obfuscated # non-TLS bytes — we literally cannot decrypt these, and # since the target IP is blocked we can't direct-tunnel - # either. The only workaround is to configure Telegram as - # an HTTP proxy (not SOCKS5), so it sends hostnames our - # SNI-rewrite path can handle. + # either. Telegram will rotate to another DC on its own; + # failing fast here lets that happen sooner. # • Client CONNECTs but never speaks TLS (some probes). if _is_ip_literal(host) and port == 443: - log.warning( - "MITM TLS handshake failed for %s:%d (%s). " - "Likely non-TLS traffic (e.g. Telegram MTProto over " - "SOCKS5). Cannot relay raw TCP to a blocked IP — " - "use the HTTP proxy instead so hostnames are preserved.", - host, port, e, + log.info( + "Non-TLS traffic on %s:%d (likely Telegram MTProto / " + "obfuscated protocol). This DC appears blocked; the " + "client should rotate to another endpoint shortly.", + host, port, ) elif port != 443: log.debug(