fix socks
This commit is contained in:
@@ -12,6 +12,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
@@ -140,6 +141,17 @@ def _is_addr_in_use_error(exc: OSError) -> bool:
|
||||
)
|
||||
|
||||
|
||||
def _bind_target_from_error(exc: OSError, config: dict) -> tuple[str, int]:
|
||||
text = str(exc)
|
||||
match = re.search(r"\('([^']+)',\s*(\d+)\)", text)
|
||||
if match:
|
||||
return match.group(1), int(match.group(2))
|
||||
return (
|
||||
config.get("listen_host", "127.0.0.1"),
|
||||
config.get("listen_port", 8080),
|
||||
)
|
||||
|
||||
|
||||
def main():
|
||||
args = parse_args()
|
||||
config_path = args.config
|
||||
@@ -216,6 +228,13 @@ def main():
|
||||
mode = config.get("mode", "domain_fronting")
|
||||
log.info("DomainFront Tunnel starting (mode: %s)", mode)
|
||||
|
||||
if config.get("socks5_enabled"):
|
||||
log.info(
|
||||
"SOCKS5 address : %s:%d",
|
||||
config.get("listen_host", "127.0.0.1"),
|
||||
config.get("socks5_port", 1080),
|
||||
)
|
||||
|
||||
if mode == "custom_domain":
|
||||
log.info("Custom domain : %s", config["custom_domain"])
|
||||
elif mode == "google_fronting":
|
||||
@@ -263,8 +282,7 @@ def main():
|
||||
asyncio.run(ProxyServer(config).start())
|
||||
except OSError as e:
|
||||
if _is_addr_in_use_error(e):
|
||||
host = config.get("listen_host", "127.0.0.1")
|
||||
port = config.get("listen_port", 8080)
|
||||
host, port = _bind_target_from_error(e, config)
|
||||
log.error("Cannot listen on %s:%d because that address is already in use.", host, port)
|
||||
details = _windows_listener_details(host, port)
|
||||
if details:
|
||||
|
||||
+140
-15
@@ -7,11 +7,14 @@ a domain-fronted connection to a CDN worker or Apps Script relay.
|
||||
Supports:
|
||||
- CONNECT method → WebSocket tunnel (modes 1-3) or MITM relay (apps_script)
|
||||
- GET / POST etc. → HTTP forwarding (modes 1-3) or JSON relay (apps_script)
|
||||
- SOCKS5 CONNECT → Same tunnel/MITM routing as the HTTP proxy
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import contextlib
|
||||
import logging
|
||||
import re
|
||||
import socket
|
||||
import ssl
|
||||
import time
|
||||
|
||||
@@ -104,6 +107,8 @@ 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.socks5_enabled = bool(config.get("socks5_enabled"))
|
||||
self.socks5_port = int(config.get("socks5_port", 1080))
|
||||
self.mode = config.get("mode", "domain_fronting")
|
||||
self.fronter = DomainFronter(config)
|
||||
self.mitm = None
|
||||
@@ -128,13 +133,33 @@ class ProxyServer:
|
||||
raise SystemExit(1)
|
||||
|
||||
async def start(self):
|
||||
srv = await asyncio.start_server(self._on_client, self.host, self.port)
|
||||
log.info(
|
||||
"Listening on %s:%d — configure your browser HTTP proxy to this address",
|
||||
self.host, self.port,
|
||||
)
|
||||
async with srv:
|
||||
await srv.serve_forever()
|
||||
servers = []
|
||||
try:
|
||||
http_srv = await asyncio.start_server(self._on_client, self.host, self.port)
|
||||
servers.append(http_srv)
|
||||
log.info(
|
||||
"Listening on %s:%d — configure your browser HTTP proxy to this address",
|
||||
self.host, self.port,
|
||||
)
|
||||
|
||||
if self.socks5_enabled:
|
||||
socks_srv = await asyncio.start_server(
|
||||
self._on_socks5_client, self.host, self.socks5_port
|
||||
)
|
||||
servers.append(socks_srv)
|
||||
log.info(
|
||||
"Listening on %s:%d — SOCKS5 CONNECT (no-auth, TCP only)",
|
||||
self.host, self.socks5_port,
|
||||
)
|
||||
|
||||
async with contextlib.AsyncExitStack() as stack:
|
||||
for srv in servers:
|
||||
await stack.enter_async_context(srv)
|
||||
await asyncio.gather(*(srv.serve_forever() for srv in servers))
|
||||
finally:
|
||||
for srv in servers:
|
||||
srv.close()
|
||||
await asyncio.gather(*(srv.wait_closed() for srv in servers), return_exceptions=True)
|
||||
|
||||
# ── client handler ────────────────────────────────────────────
|
||||
|
||||
@@ -176,19 +201,79 @@ class ProxyServer:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def _on_socks5_client(self, reader: asyncio.StreamReader, writer: asyncio.StreamWriter):
|
||||
addr = writer.get_extra_info("peername")
|
||||
try:
|
||||
version, nmethods = await asyncio.wait_for(reader.readexactly(2), timeout=15)
|
||||
if version != 5:
|
||||
return
|
||||
|
||||
methods = await asyncio.wait_for(reader.readexactly(nmethods), timeout=15)
|
||||
if 0x00 not in methods:
|
||||
writer.write(b"\x05\xff")
|
||||
await writer.drain()
|
||||
return
|
||||
|
||||
writer.write(b"\x05\x00")
|
||||
await writer.drain()
|
||||
|
||||
version, cmd, _rsv, atyp = await asyncio.wait_for(reader.readexactly(4), timeout=30)
|
||||
if version != 5:
|
||||
return
|
||||
|
||||
host = await self._read_socks5_address(reader, atyp)
|
||||
if host is None:
|
||||
await self._send_socks5_reply(writer, rep=0x08)
|
||||
return
|
||||
port = int.from_bytes(
|
||||
await asyncio.wait_for(reader.readexactly(2), timeout=15), "big"
|
||||
)
|
||||
|
||||
if cmd != 0x01:
|
||||
log.debug("SOCKS5 unsupported command %d from %s", cmd, addr)
|
||||
await self._send_socks5_reply(writer, rep=0x07)
|
||||
return
|
||||
|
||||
await self._handle_connect_target(
|
||||
host,
|
||||
port,
|
||||
reader,
|
||||
writer,
|
||||
protocol="SOCKS5",
|
||||
ready_cb=lambda: self._send_socks5_reply(writer, rep=0x00),
|
||||
)
|
||||
|
||||
except asyncio.IncompleteReadError:
|
||||
pass
|
||||
except asyncio.TimeoutError:
|
||||
log.debug("SOCKS5 timeout: %s", addr)
|
||||
except Exception as e:
|
||||
log.error("SOCKS5 error (%s): %s", addr, e)
|
||||
finally:
|
||||
try:
|
||||
writer.close()
|
||||
await writer.wait_closed()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# ── CONNECT (HTTPS tunnelling) ────────────────────────────────
|
||||
|
||||
async def _do_connect(self, target: str, reader, writer):
|
||||
host, _, port = target.rpartition(":")
|
||||
port = int(port) if port else 443
|
||||
if not host:
|
||||
host, port = target, 443
|
||||
host, port = self._split_host_port(target, default_port=443)
|
||||
await self._handle_connect_target(
|
||||
host,
|
||||
port,
|
||||
reader,
|
||||
writer,
|
||||
protocol="CONNECT",
|
||||
ready_cb=lambda: self._send_http_connect_ok(writer),
|
||||
)
|
||||
|
||||
log.info("CONNECT → %s:%d", host, port)
|
||||
|
||||
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
await writer.drain()
|
||||
async def _handle_connect_target(self, host: str, port: int, reader, writer,
|
||||
*, protocol: str, ready_cb):
|
||||
log.info("%s → %s:%d", protocol, host, port)
|
||||
|
||||
await ready_cb()
|
||||
if self.mode == "apps_script":
|
||||
override_ip = self._sni_rewrite_ip(host)
|
||||
if override_ip:
|
||||
@@ -207,6 +292,46 @@ class ProxyServer:
|
||||
else:
|
||||
await self.fronter.tunnel(host, port, reader, writer)
|
||||
|
||||
@staticmethod
|
||||
def _split_host_port(target: str, default_port: int) -> tuple[str, int]:
|
||||
target = target.strip()
|
||||
if target.startswith("["):
|
||||
host, _, port_str = target[1:].partition("]:")
|
||||
return host, int(port_str) if port_str else default_port
|
||||
host, _, port_str = target.rpartition(":")
|
||||
if host:
|
||||
return host, int(port_str)
|
||||
return target, default_port
|
||||
|
||||
@staticmethod
|
||||
async def _send_http_connect_ok(writer):
|
||||
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
await writer.drain()
|
||||
|
||||
@staticmethod
|
||||
async def _read_socks5_address(reader: asyncio.StreamReader, atyp: int) -> str | None:
|
||||
if atyp == 0x01:
|
||||
return socket.inet_ntoa(await reader.readexactly(4))
|
||||
if atyp == 0x03:
|
||||
length = (await reader.readexactly(1))[0]
|
||||
return (await reader.readexactly(length)).decode("ascii", errors="replace")
|
||||
if atyp == 0x04:
|
||||
return socket.inet_ntop(socket.AF_INET6, await reader.readexactly(16))
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def _send_socks5_reply(writer, *, rep: int,
|
||||
bind_host: str = "0.0.0.0", bind_port: int = 0):
|
||||
try:
|
||||
packed_host = socket.inet_aton(bind_host)
|
||||
atyp = 0x01
|
||||
except OSError:
|
||||
packed_host = b"\x00\x00\x00\x00"
|
||||
atyp = 0x01
|
||||
reply = b"\x05" + bytes([rep, 0x00, atyp]) + packed_host + bind_port.to_bytes(2, "big")
|
||||
writer.write(reply)
|
||||
await writer.drain()
|
||||
|
||||
# ── Hosts override (fake DNS) ─────────────────────────────────
|
||||
|
||||
# Built-in list of domains that must be reached via Google's frontend IP
|
||||
|
||||
Reference in New Issue
Block a user