refactor: update configuration keys and improve documentation for HTTP proxy settings

This commit is contained in:
Abolfazl
2026-05-05 06:47:51 +03:30
parent 603e96b631
commit e9fda55adf
9 changed files with 140 additions and 130 deletions
+30 -8
View File
@@ -155,14 +155,12 @@ It'll prompt for your Deployment ID, generate a random `auth_key`, and write
2. Open `config.json` in any text editor and fill in your values:
```json
{
"mode": "apps_script",
"google_ip": "216.239.38.120",
"front_domain": "www.google.com",
"script_id": "PASTE_YOUR_DEPLOYMENT_ID_HERE",
"auth_key": "your-secret-password-here",
"listen_host": "127.0.0.1",
"listen_port": 8085,
"socks5_enabled": true,
"http_port": 8085,
"socks5_port": 1080,
"log_level": "INFO",
"verify_ssl": true
@@ -301,7 +299,7 @@ By default, the proxy only listens on `127.0.0.1` (localhost), meaning only your
{
"lan_sharing": true,
"listen_host": "0.0.0.0",
"listen_port": 8085
"http_port": 8085
}
```
@@ -326,7 +324,7 @@ This project is centered on the **Apps Script** relay (free, no VPS needed). For
| `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, `0.0.0.0` = all interfaces for LAN sharing) |
| `listen_port` | Which port to listen on (default: `8085`) |
| `http_port` | Which HTTP proxy 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` |
@@ -340,21 +338,46 @@ This project is centered on the **Apps Script** relay (free, no VPS needed). For
| `relay_timeout` | `25` | Total timeout for one relayed request before it fails |
| `tls_connect_timeout` | `15` | Timeout for the proxy's TLS connection to the fronted Google/CDN endpoint |
| `tcp_connect_timeout` | `10` | Timeout for direct TCP tunnels and outbound SNI-rewrite connects |
| `max_response_body_bytes` | `209715200` | Hard cap for a single relay response body after buffering/decoding |
| `script_ids` | — | Multiple Script IDs for load balancing (array) |
| `chunked_download_extensions` | see [config.example.json](config.example.json) | File extensions that should use parallel range downloading. Supports `".*"` to probe all GET downloads. |
| `chunked_download_min_size` | `5242880` | Minimum total file size (5 MB) before range-parallel download stays enabled |
| `chunked_download_chunk_size` | `524288` | Per-range chunk size used by parallel downloads |
| `chunked_download_max_parallel` | `8` | Maximum simultaneous range requests for one download |
| `chunked_download_max_chunks` | `256` | Soft upper bound for total chunk requests; chunk size is raised automatically for very large files |
| `hosts` | `{}` | Manual DNS override map (`hostname` or `.suffix` -> IP). Example: `{ "example.org": "93.184.216.34", ".internal.lan": "192.168.1.10" }`. |
| `block_hosts` | `[]` | Hosts that must never be tunneled (return HTTP 403). Supports exact names (`ads.example.com`) or leading-dot suffixes (`.doubleclick.net`). |
| `direct_hosts` | `[]` | Hosts that must always go direct (no MITM and no relay/domain-fronting). Supports exact names and leading-dot suffixes. |
| `bypass_hosts` | `["localhost", ".local", ".lan", ".home.arpa"]` | Hosts that go direct (no MITM, no relay). Useful for LAN resources or sites that break under MITM. |
| `direct_google_exclude` | see [config.example.json](config.example.json) | Google apps that must use the MITM relay path instead of the fast direct tunnel. |
| `hosts` | `{}` | Manual DNS override: map a hostname to a specific IP. |
| `youtube_via_relay` | `false` | Route YouTube (`youtube.com`, `youtu.be`, `youtube-nocookie.com`) through the Apps Script relay instead of the SNI-rewrite path. The SNI-rewrite path uses Google's frontend IP which enforces SafeSearch and can cause **"Video Unavailable"** errors. Setting this to `true` fixes playback at the cost of using more Apps Script executions and slightly higher latency. |
| `exit_node.provider` | `cloudflare` | Selected exit-node backend: `cloudflare`, `deno`, `vps`, or `custom`. |
| `exit_node.url` | `""` | Beginner-friendly single URL for the selected provider. |
Practical host-policy example:
```json
{
"block_hosts": [
"ads.example.com",
".doubleclick.net"
],
"direct_hosts": [
"chat.openai.com",
".openai.com"
],
"hosts": {
"example.org": "93.184.216.34",
".internal.lan": "192.168.1.10"
}
}
```
- `block_hosts`: deny requests entirely (`403`) for exact names or full suffix trees.
- `direct_hosts`: force plain direct tunnel only (no MITM, no relay fronting).
- `hosts`: force DNS mapping before any real lookup (useful for testing/split-DNS workarounds).
Note: the relay response body cap is now a code constant (`MAX_RESPONSE_BODY_BYTES`) in [src/core/constants.py](src/core/constants.py), not a user config key.
### Optional Dependencies
Install everything from [`requirements.txt`](requirements.txt). All listed packages are optional — the proxy runs with no third-party dependencies in basic modes, but without them you lose features:
@@ -395,7 +418,6 @@ If you change `Code.gs`, you must **create a new deployment** in Google Apps Scr
python3 main.py # Normal start
python3 main.py -p 9090 # Use HTTP port 9090 instead
python3 main.py --socks5-port 1081 # Use SOCKS5 port 1081
python3 main.py --disable-socks5 # Disable SOCKS5 listener
python3 main.py --log-level DEBUG # Show detailed logs
python3 main.py -c /path/to/config.json # Use a different config file
python3 main.py --install-cert # Install MITM CA certificate and exit
+30 -7
View File
@@ -116,14 +116,12 @@ cp config.example.json config.json
```json
{
"mode": "apps_script",
"google_ip": "216.239.38.120",
"front_domain": "www.google.com",
"script_id": "PASTE_YOUR_DEPLOYMENT_ID_HERE",
"auth_key": "your-secret-password-here",
"listen_host": "127.0.0.1",
"listen_port": 8085,
"socks5_enabled": true,
"http_port": 8085,
"socks5_port": 1080,
"log_level": "INFO",
"verify_ssl": true
@@ -254,7 +252,7 @@ json
{
"lan_sharing": true,
"listen_host": "0.0.0.0",
"listen_port": 8085
"http_port": 8085
}
**هشدار امنیتی:** وقتی اشتراک‌گذاری در شبکه محلی فعال باشد، هر کسی در شبکه محلی شما می‌تواند از پروکسی شما استفاده کند. اطمینان حاصل کنید که شبکه شما مورد اعتماد است و اقدامات امنیتی بیشتری را در نظر بگیرید.
@@ -270,7 +268,7 @@ json
| `auth_key` | رمز مشترک بین کامپیوتر شما و رله |
| `script_id` | شناسه Deployment مربوط به Google Apps Script شما |
| `listen_host` | محل گوش دادن (`127.0.0.1` = فقط همین کامپیوتر، `0.0.0.0` = همه اینترفیس‌ها برای اشتراک‌گذاری LAN) |
| `listen_port` | پورتی که پروکسی روی آن اجرا می‌شود (پیش‌فرض: `8085`) |
| `http_port` | پورت HTTP پروکسی (پیش‌فرض: `8085`) |
| `lan_sharing` | فعال‌سازی اشتراک‌گذاری LAN تا دستگاه‌های دیگر در شبکه شما بتوانند از پروکسی استفاده کنند (به‌صورت پیش‌فرض `false`) |
| `log_level` | میزان جزئیات لاگ: `DEBUG`، `INFO`، `WARNING`، `ERROR` |
@@ -284,20 +282,46 @@ json
| `relay_timeout` | `25` | مهلت کل برای هر درخواست relay قبل از fail شدن |
| `tls_connect_timeout` | `15` | مهلت اتصال TLS پروکسی به endpoint fronted روی Google/CDN |
| `tcp_connect_timeout` | `10` | مهلت اتصال برای tunnel مستقیم و SNI-rewrite |
| `max_response_body_bytes` | `209715200` | سقف نهایی برای اندازه body هر پاسخ relay بعد از buffer/decode |
| `script_ids` | - | چند Deployment ID برای load balancing |
| `chunked_download_extensions` | مطابق [config.example.json](config.example.json) | پسوند فایل‌هایی که باید از دانلود range-parallel استفاده کنند. از `".*"` هم برای probe همه دانلودهای GET پشتیبانی می‌شود. |
| `chunked_download_min_size` | `5242880` | حداقل اندازه کل فایل (۵ مگابایت) برای فعال ماندن دانلود موازی |
| `chunked_download_chunk_size` | `524288` | اندازه هر chunk در دانلود موازی |
| `chunked_download_max_parallel` | `8` | حداکثر تعداد range request همزمان برای یک دانلود |
| `chunked_download_max_chunks` | `256` | سقف نرم برای تعداد کل chunk request ها؛ برای فایل‌های خیلی بزرگ اندازه chunk به‌صورت خودکار بیشتر می‌شود |
| `hosts` | `{}` | نگاشت دستی DNS (`hostname` یا `.suffix` به IP). مثال: `{ "example.org": "93.184.216.34", ".internal.lan": "192.168.1.10" }`. |
| `block_hosts` | `[]` | هاست‌هایی که هرگز نباید tunnel شوند (پاسخ 403). نام دقیق (`ads.example.com`) یا پسوند با نقطه‌ی ابتدایی (`.doubleclick.net`). |
| `direct_hosts` | `[]` | دامنه‌هایی که همیشه باید مستقیم بروند (بدون MITM و بدون relay/domain-fronting). از نام دقیق یا پسوند نقطه‌دار پشتیبانی می‌کند. |
| `bypass_hosts` | `["localhost", ".local", ".lan", ".home.arpa"]` | هاست‌هایی که مستقیم می‌روند (بدون MITM و بدون رله). برای منابع داخلی شبکه یا سایت‌هایی که با MITM مشکل دارند. |
| `direct_google_exclude` | مراجعه به [config.example.json](config.example.json) | اپ‌های Google که باید از مسیر MITM برای رله استفاده کنند به‌جای tunnel مستقیم. |
| `youtube_via_relay` | `false` | مسیردهی YouTube (`youtube.com`، `youtu.be`، `youtube-nocookie.com`) از طریق رله Apps Script به‌جای مسیر SNI-rewrite. مسیر SNI-rewrite از IP فرانت‌اند Google عبور می‌کند که SafeSearch را اجباری می‌کند و می‌تواند باعث خطای **«ویدیو در دسترس نیست»** شود. با فعال کردن این گزینه، پخش ویدیو درست می‌شود اما تعداد اجراهای Apps Script بیشتر و تأخیر اندکی بالاتر می‌رود. |
| `exit_node.provider` | `cloudflare` | backend انتخاب‌شده برای exit node: `cloudflare`، `deno`، `vps` یا `custom`. |
| `exit_node.url` | `""` | آدرس ساده و اصلی برای provider انتخاب‌شده. |
نمونه کاربردی برای policy ها:
```json
{
"block_hosts": [
"ads.example.com",
".doubleclick.net"
],
"direct_hosts": [
"chat.openai.com",
".openai.com"
],
"hosts": {
"example.org": "93.184.216.34",
".internal.lan": "192.168.1.10"
}
}
```
- `block_hosts`: این دامنه‌ها کامل مسدود می‌شوند (پاسخ `403`).
- `direct_hosts`: این دامنه‌ها همیشه مستقیم می‌روند (بدون MITM و بدون relay fronting).
- `hosts`: قبل از DNS واقعی، نگاشت دستی اعمال می‌شود (برای تست یا split-DNS workaround).
نکته: سقف اندازه پاسخ relay حالا یک مقدار ثابت کدی (`MAX_RESPONSE_BODY_BYTES`) در [src/core/constants.py](src/core/constants.py) است و دیگر گزینه‌ی کاربری config نیست.
### وابستگی‌های اختیاری
همه وابستگی‌های [`requirements.txt`](requirements.txt) اختیاری هستند — در حالت پایه بدون هیچ‌کدام کار می‌کند، ولی با نصب آن‌ها امکانات بیشتری در دسترس است:
@@ -337,7 +361,6 @@ json
python3 main.py
python3 main.py -p 9090
python3 main.py --socks5-port 1081
python3 main.py --disable-socks5
python3 main.py --log-level DEBUG
python3 main.py -c /path/to/config.json
python3 main.py --install-cert # نصب گواهی CA و خروج
+16 -75
View File
@@ -1,90 +1,30 @@
{
"mode": "apps_script",
"google_ip": "216.239.38.120",
"front_domain": "www.google.com",
"script_id": "YOUR_APPS_SCRIPT_DEPLOYMENT_ID",
"auth_key": "CHANGE_ME_TO_A_STRONG_SECRET",
"listen_host": "127.0.0.1",
"socks5_enabled": true,
"listen_port": 8085,
"http_port": 8085,
"socks5_port": 1080,
"log_level": "INFO",
"verify_ssl": true,
"lan_sharing": true,
"lan_sharing": false,
"relay_timeout": 25,
"tls_connect_timeout": 15,
"tcp_connect_timeout": 10,
"max_response_body_bytes": 209715200,
"parallel_relay": 1,
"chunked_download_extensions": [
".bin",
".zip",
".tar",
".gz",
".bz2",
".xz",
".7z",
".rar",
".exe",
".msi",
".dmg",
".deb",
".rpm",
".apk",
".iso",
".img",
".mp4",
".mkv",
".avi",
".mov",
".webm",
".mp3",
".flac",
".wav",
".aac",
".pdf",
".doc",
".docx",
".ppt",
".pptx",
".wasm"
"block_hosts": [
"ads.example.com",
".doubleclick.net"
],
"chunked_download_min_size": 5242880,
"chunked_download_chunk_size": 524288,
"chunked_download_max_parallel": 8,
"chunked_download_max_chunks": 256,
"block_hosts": [],
"bypass_hosts": [
"localhost",
".local",
".lan",
".home.arpa"
],
"direct_google_exclude": [
"gemini.google.com",
"aistudio.google.com",
"notebooklm.google.com",
"labs.google.com",
"meet.google.com",
"accounts.google.com",
"ogs.google.com",
"mail.google.com",
"calendar.google.com",
"drive.google.com",
"docs.google.com",
"chat.google.com",
"maps.google.com",
"play.google.com",
"translate.google.com",
"assistant.google.com",
"lens.google.com"
],
"direct_google_allow": [
"www.google.com",
"safebrowsing.google.com"
"direct_hosts": [
"rubika.ir",
"soft98.ir"
],
"youtube_via_relay": false,
"hosts": {},
"hosts": {
"example.org": "93.184.216.34",
".internal.lan": "192.168.1.10"
},
"exit_node": {
"enabled": false,
"provider": "cloudflare",
@@ -92,8 +32,9 @@
"psk": "",
"mode": "full",
"hosts": [
"example.com",
"example.org"
".chatgpt.com",
".openai.com"
]
}
},
"log_level": "INFO"
}
+18 -7
View File
@@ -50,7 +50,7 @@ def parse_args():
"-p", "--port",
type=int,
default=None,
help="Override listen port (env: DFT_PORT)",
help="Override HTTP proxy port (env: DFT_HTTP_PORT, legacy: DFT_PORT)",
)
parser.add_argument(
"--host",
@@ -66,7 +66,7 @@ def parse_args():
parser.add_argument(
"--disable-socks5",
action="store_true",
help="Disable the built-in SOCKS5 listener.",
help="Deprecated: SOCKS5 listener is always enabled.",
)
parser.add_argument(
"--log-level",
@@ -170,9 +170,15 @@ def main():
# CLI argument overrides
if args.port is not None:
config["listen_port"] = args.port
config["http_port"] = args.port
elif os.environ.get("DFT_HTTP_PORT"):
config["http_port"] = int(os.environ["DFT_HTTP_PORT"])
elif os.environ.get("DFT_PORT"):
config["listen_port"] = int(os.environ["DFT_PORT"])
config["http_port"] = int(os.environ["DFT_PORT"])
# Backward compatibility for older config files.
if "http_port" not in config:
config["http_port"] = int(config.get("listen_port", 8080))
if args.host is not None:
config["listen_host"] = args.host
@@ -185,7 +191,12 @@ def main():
config["socks5_port"] = int(os.environ["DFT_SOCKS5_PORT"])
if args.disable_socks5:
config["socks5_enabled"] = False
logging.getLogger("Main").warning(
"--disable-socks5 is deprecated and ignored: SOCKS5 is always enabled."
)
# Keep runtime behavior fixed regardless of user config values.
config["socks5_enabled"] = True
if args.log_level is not None:
config["log_level"] = args.log_level
@@ -273,8 +284,8 @@ def main():
# print concrete IPv4 addresses users can use on other devices.
lan_mode = lan_sharing or listen_host in ("0.0.0.0", "::")
if lan_mode:
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)
socks_port = config.get("socks5_port", 1080)
log_lan_access(config.get("http_port", config.get("listen_port", 8080)), socks_port)
try:
asyncio.run(_run(config))
+14 -20
View File
@@ -77,12 +77,10 @@ def load_base_config() -> dict:
except Exception:
pass
return {
"mode": "apps_script",
"google_ip": "216.239.38.120",
"front_domain": "www.google.com",
"listen_host": "127.0.0.1",
"listen_port": 8085,
"socks5_enabled": True,
"http_port": 8085,
"socks5_port": 1080,
"log_level": "INFO",
"verify_ssl": True,
@@ -90,11 +88,7 @@ def load_base_config() -> dict:
"relay_timeout": 25,
"tls_connect_timeout": 15,
"tcp_connect_timeout": 10,
"max_response_body_bytes": 200 * 1024 * 1024,
"chunked_download_min_size": 5 * 1024 * 1024,
"chunked_download_chunk_size": 512 * 1024,
"chunked_download_max_parallel": 8,
"chunked_download_max_chunks": 256,
"direct_hosts": [],
"hosts": {},
}
@@ -137,20 +131,21 @@ def configure_network(cfg: dict) -> dict:
default_host = "0.0.0.0"
cfg["listen_host"] = prompt("Listen host", default=default_host)
port = prompt("HTTP proxy port", default=str(cfg.get("listen_port", 8085)))
port = prompt(
"HTTP proxy port",
default=str(cfg.get("http_port", cfg.get("listen_port", 8085))),
)
try:
cfg["listen_port"] = int(port)
cfg["http_port"] = int(port)
except ValueError:
cfg["listen_port"] = 8085
cfg["http_port"] = 8085
socks5 = prompt_yes_no("Enable SOCKS5 proxy?", default=bool(cfg.get("socks5_enabled", True)))
cfg["socks5_enabled"] = socks5
if socks5:
sport = prompt("SOCKS5 port", default=str(cfg.get("socks5_port", 1080)))
try:
cfg["socks5_port"] = int(sport)
except ValueError:
cfg["socks5_port"] = 1080
# SOCKS5 is always enabled at runtime; only port is configurable.
sport = prompt("SOCKS5 port", default=str(cfg.get("socks5_port", 1080)))
try:
cfg["socks5_port"] = int(sport)
except ValueError:
cfg["socks5_port"] = 1080
return cfg
@@ -175,7 +170,6 @@ def main() -> int:
return 0
cfg = load_base_config()
cfg["mode"] = "apps_script"
suggested_key = random_auth_key()
print()
+11
View File
@@ -107,6 +107,17 @@ FRONT_SNI_POOL_GOOGLE: tuple[str, ...] = (
)
# ── Bypass hosts (direct, no MITM/relay) ────────────────────────────────
# Applied when bypass_hosts is omitted from config.json.
# Advanced users can override this list in config.json under "bypass_hosts".
DEFAULT_BYPASS_HOSTS: tuple[str, ...] = (
"localhost",
".local",
".lan",
".home.arpa",
)
# ── Per-host stats ────────────────────────────────────────────────────────
STATS_LOG_INTERVAL = 300.0 # seconds — how often to log per-host totals
STATS_LOG_TOP_N = 10 # how many hosts to include in the log
+16 -7
View File
@@ -22,6 +22,7 @@ except Exception: # optional dependency fallback
from core.constants import (
CACHE_MAX_MB,
CLIENT_IDLE_TIMEOUT,
DEFAULT_BYPASS_HOSTS,
GOOGLE_DIRECT_ALLOW_EXACT,
GOOGLE_DIRECT_ALLOW_SUFFIXES,
GOOGLE_DIRECT_EXACT_EXCLUDE,
@@ -73,14 +74,15 @@ class ProxyServer:
def __init__(self, config: dict):
self.host = config.get("listen_host", "127.0.0.1")
self.port = config.get("listen_port", 8080)
self.socks_enabled = config.get("socks5_enabled", True)
# Prefer the new key (http_port) but keep listen_port for old configs.
self.port = config.get("http_port", config.get("listen_port", 8080))
self.socks_enabled = True
self.socks_host = config.get("socks5_host", self.host)
self.socks_port = config.get("socks5_port", 1080)
if self.socks_enabled and self.socks_host == self.host \
and int(self.socks_port) == int(self.port):
raise ValueError(
f"listen_port and socks5_port must differ on the same host "
f"http_port and socks5_port must differ on the same host "
f"(both set to {self.port} on {self.host}). "
f"Change one of them in config.json."
)
@@ -137,12 +139,19 @@ class ProxyServer:
}
# ── Per-host policy ────────────────────────────────────────
# block_hosts — refuse traffic entirely (close or 403)
# bypass_hosts — route directly (no MITM, no relay)
# block_hosts — refuse traffic entirely (close or 403)
# direct_hosts — route directly (no MITM, no relay)
# bypass_hosts — legacy alias kept for backward compatibility
# Both accept exact hostnames and leading-dot suffix patterns,
# e.g. ".local" matches any *.local domain.
self._block_hosts = load_host_rules(config.get("block_hosts", []))
self._bypass_hosts = load_host_rules(config.get("bypass_hosts", []))
direct_hosts = config.get("direct_hosts", [])
bypass_hosts = config.get("bypass_hosts")
if bypass_hosts is None:
bypass_hosts = list(DEFAULT_BYPASS_HOSTS)
self._bypass_hosts = load_host_rules(
list(bypass_hosts) + list(direct_hosts)
)
# Route YouTube through the relay when requested; the Google frontend
# IP can enforce SafeSearch on the SNI-rewrite path.
@@ -428,7 +437,7 @@ class ProxyServer:
return
if self._is_bypassed(host):
log.info("Bypass tunnel → %s:%d (matches bypass_hosts)", host, port)
log.info("Direct tunnel → %s:%d (matches direct_hosts/bypass_hosts)", host, port)
await self._do_direct_tunnel(host, port, reader, writer)
return
+4 -5
View File
@@ -137,10 +137,9 @@ class DomainFronter:
config, "tls_connect_timeout", TLS_CONNECT_TIMEOUT, minimum=1.0,
)
self._sni_probe_timeout = min(self._tls_connect_timeout, 4.0)
self._max_response_body_bytes = self._cfg_int(
config, "max_response_body_bytes", MAX_RESPONSE_BODY_BYTES,
minimum=1024,
)
# Keep response cap as a code-level constant to avoid exposing an
# advanced memory-safety knob in end-user config.
self._max_response_body_bytes = MAX_RESPONSE_BODY_BYTES
# Connection pool — TTL-based, pre-warmed, with concurrency control
self._pool: list[tuple[asyncio.StreamReader, asyncio.StreamWriter, float]] = []
@@ -1418,7 +1417,7 @@ class DomainFronter:
502,
"Relay response exceeds cap "
f"({self._max_response_body_bytes} bytes). "
"Increase max_response_body_bytes if your system has enough RAM.",
"Increase MAX_RESPONSE_BODY_BYTES in src/core/constants.py if your system has enough RAM.",
)
if min_size > 0 and total_size < min_size:
return self._rewrite_206_to_200(first_resp)
+1 -1
View File
@@ -224,7 +224,7 @@ def parse_relay_json(data: dict, max_body_bytes: int) -> bytes:
return error_response(
502,
f"Relay response exceeds cap ({max_body_bytes} bytes). "
"Increase max_response_body_bytes if your system has enough RAM.",
"Increase MAX_RESPONSE_BODY_BYTES in src/core/constants.py if your system has enough RAM.",
)
status_text = {