fix socks

This commit is contained in:
asus
2026-04-22 19:28:25 +03:30
parent 3c20028212
commit 873d72747b
2 changed files with 160 additions and 17 deletions
+20 -2
View File
@@ -12,6 +12,7 @@ import asyncio
import json import json
import logging import logging
import os import os
import re
import subprocess import subprocess
import sys 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(): def main():
args = parse_args() args = parse_args()
config_path = args.config config_path = args.config
@@ -216,6 +228,13 @@ def main():
mode = config.get("mode", "domain_fronting") mode = config.get("mode", "domain_fronting")
log.info("DomainFront Tunnel starting (mode: %s)", mode) 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": if mode == "custom_domain":
log.info("Custom domain : %s", config["custom_domain"]) log.info("Custom domain : %s", config["custom_domain"])
elif mode == "google_fronting": elif mode == "google_fronting":
@@ -263,8 +282,7 @@ def main():
asyncio.run(ProxyServer(config).start()) asyncio.run(ProxyServer(config).start())
except OSError as e: except OSError as e:
if _is_addr_in_use_error(e): if _is_addr_in_use_error(e):
host = config.get("listen_host", "127.0.0.1") host, port = _bind_target_from_error(e, config)
port = config.get("listen_port", 8080)
log.error("Cannot listen on %s:%d because that address is already in use.", host, port) log.error("Cannot listen on %s:%d because that address is already in use.", host, port)
details = _windows_listener_details(host, port) details = _windows_listener_details(host, port)
if details: if details:
+136 -11
View File
@@ -7,11 +7,14 @@ a domain-fronted connection to a CDN worker or Apps Script relay.
Supports: Supports:
- CONNECT method → WebSocket tunnel (modes 1-3) or MITM relay (apps_script) - 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) - 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 asyncio
import contextlib
import logging import logging
import re import re
import socket
import ssl import ssl
import time import time
@@ -104,6 +107,8 @@ class ProxyServer:
def __init__(self, config: dict): def __init__(self, config: dict):
self.host = config.get("listen_host", "127.0.0.1") self.host = config.get("listen_host", "127.0.0.1")
self.port = config.get("listen_port", 8080) 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.mode = config.get("mode", "domain_fronting")
self.fronter = DomainFronter(config) self.fronter = DomainFronter(config)
self.mitm = None self.mitm = None
@@ -128,13 +133,33 @@ class ProxyServer:
raise SystemExit(1) raise SystemExit(1)
async def start(self): async def start(self):
srv = await asyncio.start_server(self._on_client, self.host, self.port) servers = []
try:
http_srv = await asyncio.start_server(self._on_client, self.host, self.port)
servers.append(http_srv)
log.info( log.info(
"Listening on %s:%d — configure your browser HTTP proxy to this address", "Listening on %s:%d — configure your browser HTTP proxy to this address",
self.host, self.port, self.host, self.port,
) )
async with srv:
await srv.serve_forever() 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 ──────────────────────────────────────────── # ── client handler ────────────────────────────────────────────
@@ -176,19 +201,79 @@ class ProxyServer:
except Exception: except Exception:
pass 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) ──────────────────────────────── # ── CONNECT (HTTPS tunnelling) ────────────────────────────────
async def _do_connect(self, target: str, reader, writer): async def _do_connect(self, target: str, reader, writer):
host, _, port = target.rpartition(":") host, port = self._split_host_port(target, default_port=443)
port = int(port) if port else 443 await self._handle_connect_target(
if not host: host,
host, port = target, 443 port,
reader,
writer,
protocol="CONNECT",
ready_cb=lambda: self._send_http_connect_ok(writer),
)
log.info("CONNECT → %s:%d", host, port) async def _handle_connect_target(self, host: str, port: int, reader, writer,
*, protocol: str, ready_cb):
writer.write(b"HTTP/1.1 200 Connection Established\r\n\r\n") log.info("%s%s:%d", protocol, host, port)
await writer.drain()
await ready_cb()
if self.mode == "apps_script": if self.mode == "apps_script":
override_ip = self._sni_rewrite_ip(host) override_ip = self._sni_rewrite_ip(host)
if override_ip: if override_ip:
@@ -207,6 +292,46 @@ class ProxyServer:
else: else:
await self.fronter.tunnel(host, port, reader, writer) 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) ───────────────────────────────── # ── Hosts override (fake DNS) ─────────────────────────────────
# Built-in list of domains that must be reached via Google's frontend IP # Built-in list of domains that must be reached via Google's frontend IP