fix socks
This commit is contained in:
@@ -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:
|
||||||
|
|||||||
+140
-15
@@ -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 = []
|
||||||
log.info(
|
try:
|
||||||
"Listening on %s:%d — configure your browser HTTP proxy to this address",
|
http_srv = await asyncio.start_server(self._on_client, self.host, self.port)
|
||||||
self.host, self.port,
|
servers.append(http_srv)
|
||||||
)
|
log.info(
|
||||||
async with srv:
|
"Listening on %s:%d — configure your browser HTTP proxy to this address",
|
||||||
await srv.serve_forever()
|
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 ────────────────────────────────────────────
|
# ── 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
|
||||||
|
|||||||
Reference in New Issue
Block a user