From 1d48d50565adea7a55166a7410af376ae0508fc1 Mon Sep 17 00:00:00 2001 From: Emran Hejazi Date: Wed, 22 Apr 2026 21:11:58 +0330 Subject: [PATCH] Implement lan sharing --- README.md | 27 +++++++- README_FA.md | 37 +++++++++-- config.example.json | 1 + main.py | 14 ++++ requirements.txt | 3 + src/lan_utils.py | 152 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 227 insertions(+), 7 deletions(-) create mode 100644 src/lan_utils.py diff --git a/README.md b/README.md index 6c34769..cbc1ad1 100644 --- a/README.md +++ b/README.md @@ -174,6 +174,29 @@ Firefox uses its own certificate store, so even after OS-level install you need --- +## LAN Sharing (Optional) + +By default, the proxy only listens on `127.0.0.1` (localhost), meaning only your computer can use it. To allow other devices on your local network (LAN) to use the proxy: + +1. Set `"lan_sharing": true` in your `config.json` +2. The proxy will automatically listen on all network interfaces (`0.0.0.0`) +3. The startup log will show your LAN IP addresses that other devices can connect to + +**Example LAN configuration:** +```json +{ + "lan_sharing": true, + "listen_host": "0.0.0.0", + "listen_port": 8085 +} +``` + +**Security Warning:** When LAN sharing is enabled, anyone on your local network can use your proxy. Ensure your network is trusted and consider additional security measures. + +**On other devices:** Configure them to use your computer's LAN IP (shown in the startup log) and port 8085 as the HTTP proxy. + +--- + ## Modes Overview This project focuses entirely on the **Apps Script** relay — a free Google account is all you need, no server, no VPS, no Cloudflare setup. Everything is configured out of the box for this mode. @@ -188,8 +211,9 @@ This project focuses entirely on the **Apps Script** relay — a free Google acc |---------|-------------| | `auth_key` | Password shared between your computer and the relay | | `script_id` | Your Google Apps Script Deployment ID | -| `listen_host` | Where to listen (`127.0.0.1` = only this computer) | +| `listen_host` | Where to listen (`127.0.0.1` = only this computer, `0.0.0.0` = all interfaces for LAN sharing) | | `listen_port` | Which port to listen on (default: `8085`) | +| `lan_sharing` | Enable LAN sharing to allow other devices on your network to use the proxy (`false` by default) | | `log_level` | How much detail to show: `DEBUG`, `INFO`, `WARNING`, `ERROR` | ### Advanced Settings @@ -215,6 +239,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 33bbbc0..5af49ad 100644 --- a/README_FA.md +++ b/README_FA.md @@ -162,6 +162,7 @@ Firefox معمولا certificate store جداگانه دارد: نکته امنیتی: پوشه `ca/` را با کسی به اشتراک نگذارید. اگر خواستید از اول گواهی جدید بسازید، این پوشه را حذف کنید تا دوباره ساخته شود. + --- ## حالت‌های موجود @@ -170,15 +171,38 @@ Firefox معمولا certificate store جداگانه دارد: --- -## تنظیمات مهم +## اشتراک‌گذاری در شبکه محلی (اختیاری) + +به‌طور پیش‌فرض، پروکسی فقط به `127.0.0.1` (localhost) گوش می‌دهد، به این معنی که فقط کامپیوتر شما می‌تواند از آن استفاده کند. برای اینکه سایر دستگاه‌های موجود در شبکه محلی (LAN) شما بتوانند از این پروکسی استفاده کنند: + +۱. در فایل `config.json` خود، مقدار `"lan_sharing"` را `true` قرار دهید. +۲. پروکسی به طور خودکار به تمام رابط‌های شبکه (`0.0.0.0`) گوش خواهد داد. +۳. در لاگ راه‌اندازی، آدرس‌های IP شبکه محلی شما که سایر دستگاه‌ها می‌توانند به آن متصل شوند، نمایش داده می‌شود. + +**نمونه پیکربندی برای شبکه محلی:** +json +{ + "lan_sharing": true, + "listen_host": "0.0.0.0", + "listen_port": 8085 +} + +**هشدار امنیتی:** وقتی اشتراک‌گذاری در شبکه محلی فعال باشد، هر کسی در شبکه محلی شما می‌تواند از پروکسی شما استفاده کند. اطمینان حاصل کنید که شبکه شما مورد اعتماد است و اقدامات امنیتی بیشتری را در نظر بگیرید. + +**در سایر دستگاه‌ها:** آن‌ها را طوری پیکربندی کنید که از آدرس IP کامپیوتر شما در شبکه محلی (که در لاگ راه‌اندازی نمایش داده می‌شود) و پورت 8085 به عنوان پروکسی HTTP استفاده کنند. + +--- + +## تنظیمات اصلی | تنظیم | توضیح | |------|-------| -| `auth_key` | رمز مشترک بین برنامه و رله | -| `script_id` | Deployment ID مربوط به Apps Script | -| `listen_host` | آدرس محلی برای اجرا | -| `listen_port` | پورت پراکسی | -| `log_level` | میزان جزئیات لاگ | +| `auth_key` | رمز مشترک بین کامپیوتر شما و رله | +| `script_id` | شناسه Deployment مربوط به Google Apps Script شما | +| `listen_host` | محل گوش دادن (`127.0.0.1` = فقط همین کامپیوتر، `0.0.0.0` = همه اینترفیس‌ها برای اشتراک‌گذاری LAN) | +| `listen_port` | پورتی که پروکسی روی آن اجرا می‌شود (پیش‌فرض: `8085`) | +| `lan_sharing` | فعال‌سازی اشتراک‌گذاری LAN تا دستگاه‌های دیگر در شبکه شما بتوانند از پروکسی استفاده کنند (به‌صورت پیش‌فرض `false`) | +| `log_level` | میزان جزئیات لاگ: `DEBUG`، `INFO`، `WARNING`، `ERROR` | ### تنظیمات پیشرفته @@ -202,6 +226,7 @@ Firefox معمولا certificate store جداگانه دارد: | `h2` | ارتباط HTTP/2 با رله Apps Script (به‌طور محسوسی سریع‌تر) | | `brotli` | پشتیبانی از فشرده‌سازی `Content-Encoding: br` | | `zstandard` | پشتیبانی از فشرده‌سازی `Content-Encoding: zstd` | +| `netifaces` | تشخیص بهتر اینترفیس‌های شبکه برای اشتراک‌گذاری LAN (در صورت نبود آن، حالت جایگزین در دسترس است) | ### استفاده از چند Script ID diff --git a/config.example.json b/config.example.json index ebdab15..2a9be7b 100644 --- a/config.example.json +++ b/config.example.json @@ -10,6 +10,7 @@ "socks5_port": 1080, "log_level": "INFO", "verify_ssl": true, + "lan_sharing": true, "parallel_relay": 1, "block_hosts": [], "bypass_hosts": [ diff --git a/main.py b/main.py index 8d0c8fa..d6f59bf 100644 --- a/main.py +++ b/main.py @@ -22,6 +22,7 @@ if _SRC_DIR not in sys.path: from cert_installer import install_ca, is_ca_trusted from constants import __version__ +from lan_utils import log_lan_access from logging_utils import configure as configure_logging, print_banner from mitm import CA_CERT_FILE from proxy_server import ProxyServer @@ -205,6 +206,14 @@ def main(): else: log.info("MITM CA is already trusted.") + # ── LAN sharing configuration ──────────────────────────────────────── + lan_sharing = config.get("lan_sharing", True) + if lan_sharing: + # If LAN sharing is enabled and host is still localhost, change to all interfaces + if config.get("listen_host", "127.0.0.1") == "127.0.0.1": + config["listen_host"] = "0.0.0.0" + log.info("LAN sharing enabled — listening on all interfaces") + log.info("HTTP proxy : %s:%d", config.get("listen_host", "127.0.0.1"), config.get("listen_port", 8080)) @@ -213,6 +222,11 @@ def main(): config.get("socks5_host", config.get("listen_host", "127.0.0.1")), config.get("socks5_port", 1080)) + # Log LAN access addresses if sharing is enabled + if lan_sharing: + socks_port = config.get("socks5_port", 1080) if config.get("socks5_enabled", True) else None + log_lan_access(config.get("listen_port", 8080), socks_port) + try: asyncio.run(_run(config)) except KeyboardInterrupt: diff --git a/requirements.txt b/requirements.txt index 79adb46..c0f1980 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,3 +12,6 @@ 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 diff --git a/src/lan_utils.py b/src/lan_utils.py new file mode 100644 index 0000000..b8e271d --- /dev/null +++ b/src/lan_utils.py @@ -0,0 +1,152 @@ +""" +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. +""" + +import ipaddress +import logging +import socket +from typing import Dict, List, Optional + +log = logging.getLogger("LAN") + + +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. + + Returns: + Dict[str, List[str]]: Interface name -> list of IP addresses + """ + interfaces = {} + + 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 + + try: + # Get IPv6 addresses + ipv6_info = socket.getaddrinfo(hostname, None, socket.AF_INET6) + ipv6_addrs = [] + for info in ipv6_info: + 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) + + return interfaces + + +def get_lan_ips(port: int = 8085) -> List[str]: + """ + Get list of LAN-accessible proxy addresses. + + Returns a list of IP:port combinations that can be used to access + the proxy from other devices on the local network. + + Args: + port: The port the proxy is listening on + + Returns: + List[str]: List of "IP:port" strings for LAN access + """ + interfaces = get_network_interfaces() + lan_addresses = [] + + 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 + + # Remove duplicates while preserving order + seen = set() + unique_addresses = [] + for addr in lan_addresses: + if addr not in seen: + seen.add(addr) + unique_addresses.append(addr) + + return unique_addresses + + +def log_lan_access(port: int = 8085, socks_port: Optional[int] = None): + """ + Log the LAN-accessible proxy addresses for user convenience. + + Args: + port: HTTP proxy port + socks_port: Optional SOCKS5 proxy port + """ + lan_http = get_lan_ips(port) + if lan_http: + log.info("LAN HTTP proxy : %s", ", ".join(lan_http)) + else: + log.warning("No LAN IP addresses detected for HTTP proxy") + + if socks_port: + lan_socks = get_lan_ips(socks_port) + if lan_socks: + log.info("LAN SOCKS5 proxy : %s", ", ".join(lan_socks)) + else: + log.warning("No LAN IP addresses detected for SOCKS5 proxy")